Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sidebar): develop new component #2868

Merged
merged 21 commits into from
Dec 30, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions migrate-from-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,16 @@ plugins: [

- `offset` 重命名为 `indent`

#### SideBar

- 新增SideBar组件
- 支持属性value,用于当前激活的`item`的key
- 支持属性defaultValue, 表示未设置value时,`item`的key的默认值
- 支持属性contentDuration, 用于内容滚动动画时长
- 支持属性sidebarDuration, 用于侧栏滚动动画时长
- 支持属性onClick, 点击标签时触发
- 支持属性onChange, 当前激活的标签改变时触发

#### Tabbar

- `unactiveColor` 重命名为 `inactiveColor`
Expand Down Expand Up @@ -607,7 +617,7 @@ plugins: [
- 移除 `isAsync`,通过 `checked`实现
- 移除 `activeColor` ,通过css变量`--nutui-switch-open-background-color`实现
- 移除 `inactiveColor`,通过css变量`--nutui-switch-close-background-color`实现
- `activeText 属性类型更改为 `ReactNode`
- `activeText 属性类型更改为`ReactNode`
Alex-huxiyang marked this conversation as resolved.
Show resolved Hide resolved
- `inactiveText` 属性类型更改为 `ReactNode`

#### Toast
Expand Down Expand Up @@ -791,7 +801,7 @@ plugins: [
- 移除 `pageContent`,通过 indicator 实现
- `autoplay` 重命名为 `autoplay`
- `initPage` 重命名为 `defaultValue`
- `paginationVisible` 重命名为 `indicator`,类型改为` ReactNode`
- `paginationVisible` 重命名为 `indicator`,类型改为`ReactNode`
- `isPreventDefault` 重命名为 `preventDefault`
- `isStopPropagation` 重命名为 `stopPropagation`
- `isCenter` 重命名为 `center`
Expand Down
24 changes: 24 additions & 0 deletions src/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,30 @@
"taro": true,
"author": "hx"
},
{
"version": "3.0.0",
"name": "SideBar",
"type": "component",
"cName": "侧边栏导航",
"desc": "用于侧边内容选择和切换",
"sort": 10,
"show": true,
"taro": true,
"author": "Alex.hxy",
"v15": true
},
{
"version": "3.0.0",
"name": "SideBarItem",
"type": "component",
"cName": "侧边栏导航子组件",
"desc": "用于侧边内容选择和切换",
"sort": 10,
"show": false,
"taro": true,
"author": "Alex.hxy",
"v15": true
},
Alex-huxiyang marked this conversation as resolved.
Show resolved Hide resolved
{
"version": "2.0.0",
"name": "SideNavBarItem",
Expand Down
70 changes: 70 additions & 0 deletions src/packages/sidebar/_test_/sidebar.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import * as React from 'react'
import { fireEvent, render } from '@testing-library/react'
import '@testing-library/jest-dom'
import { SideBar } from '../sidebar'

const list = Array.from(new Array(3).keys())

test('should render defaultValue correctly', async () => {
const { container } = render(
<SideBar style={{ height: 300 }} value="0">
{list.map((item) => (
<SideBar.Item key={item} title={`Opt ${item + 1}`}>
Content {item + 1}
</SideBar.Item>
))}
</SideBar>
)
const item = container.querySelectorAll('.nut-sidebar-titles-item')[0]
expect(item).toHaveClass('nut-sidebar-titles-item-active')
})
Alex-huxiyang marked this conversation as resolved.
Show resolved Hide resolved

test('should choose and scroll to the right option', async () => {
const onChange = vi.fn()
const { container } = render(
<SideBar style={{ height: 300 }} value="0" onChange={onChange}>
{list.map((item) => (
<SideBar.Item key={item} title={`Opt ${item + 1}`}>
Content {item + 1}
</SideBar.Item>
))}
</SideBar>
)
const items = container.querySelectorAll('.nut-sidebar-titles-item')
fireEvent.click(items[1])
expect(onChange).toHaveBeenCalledWith(1)
})
Alex-huxiyang marked this conversation as resolved.
Show resolved Hide resolved
test('disabled option', async () => {
const onChange = vi.fn()
const { container } = render(
<SideBar style={{ height: 300 }} value="0" onChange={onChange}>
{list.map((item) => (
<SideBar.Item key={item} title={`Opt ${item + 1}`} disabled>
Content {item + 1}
</SideBar.Item>
))}
</SideBar>
)
const items = container.querySelectorAll('.nut-sidebar-titles-item')
fireEvent.click(items[1])
expect(onChange).not.toHaveBeenCalled()
})
Alex-huxiyang marked this conversation as resolved.
Show resolved Hide resolved
test('matchByValue', async () => {
const list1 = [
{ value: 'a', title: 'Opt a' },
{ value: 'b', title: 'Opt b' },
{ value: 'c', title: 'Opt c' },
]
const onChange = vi.fn()
const { container } = render(
<SideBar style={{ height: 300 }} value="b" onChange={onChange}>
{list1.map((item) => (
<SideBar.Item key={item.value} title={item.title} value={item.value}>
Content {item.value}
</SideBar.Item>
))}
</SideBar>
)
const items = container.querySelectorAll('.nut-sidebar-titles-item')
expect(items[1]).toHaveClass('nut-sidebar-titles-item-active')
})
51 changes: 51 additions & 0 deletions src/packages/sidebar/demo.taro.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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'

const TabsDemo = () => {
const [translated] = useTranslate({
'zh-CN': {
basic: '基础用法',
disabled: '禁用选项',
matchByValue: '根据value匹配',
multiTitle: '多个标题',
setDuration: '设置滚动动画时长',
},
'en-US': {
basic: 'Basic Usage',
disabled: 'Disabled',
matchByValue: 'Match By Value',
multiTitle: 'Multiple Titles',
setDuration: 'Set Scroll Animation Duration',
},
})

return (
<>
<Header />
<ScrollView
className={`demo ${Taro.getEnv() === 'WEB' ? 'web full' : ''}`}
>
<View className="h2">{translated.basic}</View>
<Demo1 />
<View className="h2">{translated.disabled}</View>
<Demo2 />
<View className="h2">{translated.matchByValue}</View>
<Demo3 />
<View className="h2">{translated.multiTitle}</View>
<Demo4 />
<View className="h2">{translated.setDuration}</View>
<Demo5 />
</ScrollView>
</>
)
}

export default TabsDemo
45 changes: 45 additions & 0 deletions src/packages/sidebar/demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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'
Alex-huxiyang marked this conversation as resolved.
Show resolved Hide resolved

const SideNavBarDemo = () => {
const [translated] = useTranslate({
'zh-CN': {
basic: '基础用法',
disabled: '禁用选项',
matchByValue: '根据value匹配',
multiTitle: '多个标题',
setDuration: '设置滚动动画时长',
},
'en-US': {
basic: 'Basic Usage',
disabled: 'Disabled',
matchByValue: 'Match By Value',
multiTitle: 'Multiple Titles',
setDuration: 'Set Scroll Animation Duration',
},
})

return (
<>
<div className="demo">
<h2>{translated.basic}</h2>
<Demo1 />
<h2>{translated.disabled}</h2>
<Demo2 />
<h2>{translated.matchByValue}</h2>
<Demo3 />
<h2>{translated.multiTitle}</h2>
<Demo4 />
<h2>{translated.setDuration}</h2>
<Demo5 />
</div>
</>
)
}

export default SideNavBarDemo
25 changes: 25 additions & 0 deletions src/packages/sidebar/demos/h5/demo1.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React, { useState } from 'react'
import { SideBar } from '@nutui/nutui-react'

const Demo1 = () => {
const [value, setValue] = useState<number | string>('0')
const list = Array.from(new Array(3).keys())
return (
<>
<SideBar
style={{ height: 300 }}
value={value}
onChange={(value) => {
setValue(value)
}}
>
{list.map((item) => (
<SideBar.Item key={item} title={`Opt ${item + 1}`}>
Content {item + 1}
</SideBar.Item>
))}
</SideBar>
</>
)
}
export default Demo1
22 changes: 22 additions & 0 deletions src/packages/sidebar/demos/h5/demo2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React, { useState } from 'react'
import { SideBar } from '@nutui/nutui-react'

const Demo2 = () => {
const [value, setValue] = useState<number | string>('0')
return (
<>
<SideBar
style={{ height: 300 }}
value={value}
onChange={(value) => {
setValue(value)
}}
>
<SideBar.Item title="Opt 1">Content 1</SideBar.Item>
<SideBar.Item title="Opt 2">Content 2</SideBar.Item>
<SideBar.Item title="Opt 3" disabled />
</SideBar>
</>
)
}
export default Demo2
28 changes: 28 additions & 0 deletions src/packages/sidebar/demos/h5/demo3.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React, { useState } from 'react'
import { SideBar } from '@nutui/nutui-react'

const Demo3 = () => {
const [value, setValue] = useState<number | string>('b')
return (
<>
<SideBar
style={{ height: 300 }}
value={value}
onChange={(value) => {
setValue(value)
}}
>
<SideBar.Item title="Opt 1" value="a">
Content 1
</SideBar.Item>
<SideBar.Item title="Opt 2" value="b">
Content 2
</SideBar.Item>
<SideBar.Item title="Opt 3" value="c">
Content 3
</SideBar.Item>
</SideBar>
</>
)
}
export default Demo3
25 changes: 25 additions & 0 deletions src/packages/sidebar/demos/h5/demo4.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React, { useState } from 'react'
import { SideBar } from '@nutui/nutui-react'

const Demo4 = () => {
const [value, setValue] = useState<number | string>('0')
const list = Array.from(new Array(20).keys())
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

建议优化大列表渲染性能

当列表项较多时,建议使用虚拟列表来优化性能。可以考虑使用 react-windowreact-virtualized 库。

另外,建议将列表数据抽离到组件外部:

+const SIDEBAR_ITEMS = Array.from(new Array(20).keys()).map((item) => ({
+  key: item,
+  title: `Opt ${item + 1}`,
+  content: `Content ${item + 1}`,
+}))

const Demo4 = () => {
  const [value, setValue] = useState<string>('0')
-  const list = Array.from(new Array(20).keys())

Also applies to: 16-20

return (
<>
<SideBar
style={{ height: 300 }}
value={value}
onChange={(value) => {
setValue(value)
}}
>
{list.map((item) => (
<SideBar.Item key={item} title={`Opt ${item + 1}`}>
Content {item + 1}
</SideBar.Item>
))}
</SideBar>
</>
)
}
export default Demo4
27 changes: 27 additions & 0 deletions src/packages/sidebar/demos/h5/demo5.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React, { useState } from 'react'
import { SideBar } from '@nutui/nutui-react'

const Demo5 = () => {
const [value, setValue] = useState<number | string>('0')
const list = Array.from(new Array(20).keys())
return (
<>
<SideBar
style={{ height: 300 }}
value={value}
contentDuration={500}
sidebarDuration={300}
onChange={(value) => {
setValue(value)
}}
>
{list.map((item) => (
<SideBar.Item key={item} title={`Opt ${item + 1}`}>
Content {item + 1}
</SideBar.Item>
))}
</SideBar>
</>
)
}
export default Demo5
25 changes: 25 additions & 0 deletions src/packages/sidebar/demos/taro/demo1.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React, { useState } from 'react'
import { SideBar } from '@nutui/nutui-react-taro'

const Demo1 = () => {
const [value, setValue] = useState<number | string>('0')
const list = Array.from(new Array(3).keys())
return (
<>
<SideBar
style={{ height: 300 }}
value={value}
onChange={(value) => {
setValue(value)
}}
>
{list.map((item) => (
<SideBar.Item key={item} title={`Opt ${item + 1}`}>
Content {item + 1}
</SideBar.Item>
))}
</SideBar>
</>
)
}
export default Demo1
Comment on lines +1 to +25
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

建议提取共享逻辑

此组件与 H5 版本存在大量重复代码,建议提取共享逻辑到一个公共的 hooks 中。

示例实现:

// useSidebarState.ts
export const useSidebarState = (initialValue: string = '0') => {
  const [value, setValue] = useState<string>(initialValue)
  const handleChange = useCallback((newValue: string) => {
    setValue(newValue)
  }, [])
  return { value, handleChange }
}

然后在组件中使用:

const Demo1 = () => {
-  const [value, setValue] = useState<number | string>('0')
+  const { value, handleChange } = useSidebarState()
   const list = Array.from(new Array(3).keys())
   return (
     <SideBar
       style={{ height: 300 }}
       value={value}
-      onChange={(value) => {
-        setValue(value)
-      }}
+      onChange={handleChange}
     >

Loading
Loading