背景
最近接到了一个需求,基于容器的宽度来决定显示的子元素数量。如果屏幕或容器足够宽,它将展示更多的子元素;如果屏幕较窄,它将隐藏一些子元素,并提供一个下拉菜单来查看这些被隐藏的内容
本来想偷懒,用gpt来做嘛,但是gpt做出来的组件库对于功能的满足不能说十全十美吧,只能一无是处,试了两个小时,放弃了,还不如自己写呢
说干就干!!!
实现思路
- 组件属性定义&初始化状态
- 动态计算和更新可见子元素数量
- 窗口尺寸变化的处理
- 渲染可见和隐藏的子元素
- 组件的最终渲染
既然有了实现思路我们就开始实现吧
实现
组件属性定义&初始化状态
- 组件属性定义 :
ResponsiveContainer
组件接收两个属性:children
和limit
。children
是任意的 React 节点,而limit
是一个可选属性,用于限制显示子元素的数量。如果没有给定limit
,组件将基于容器的宽度动态确定可见的子元素数量。 - 初始化状态 : 组件内部使用
useRef
创建两个引用:containerRef
和shadowRef
。containerRef
用于引用实际显示内容的容器,而shadowRef
用于临时存放所有子元素以计算它们的总宽度。同时,使用useState
创建一个visibleCount
状态,用来存储应该显示的子元素数量。
组件属性定义
kotlin
interface ResponsiveContainerProps {
children: ReactNode; // 期望传入任意的React节点作为children
limit?: number; // 可选属性,限制显示的子元素数量;如果未设置,将基于容器宽度动态决定
}
初始化状态
ini
const containerRef = useRef<HTMLDivElement>(null);
const [visibleCount, setVisibleCount] = useState(0);
const shadowRef = useRef<HTMLDivElement>(null);
动态计算和更新可见子元素数量
updateChildrenVisibility
函数负责计算在当前容器宽度下,可以显示多少子元素。如果提供了 limit
属性,则直接使用 limit
设定的值更新 visibleCount
状态。如果没有提供 limit
,函数将遍历 shadowRef
中的子元素,累加它们的宽度,直到达到 containerRef
容器的宽度限制。这个过程中还考虑了额外预留的空间(如12像素),确保内容不会溢出容器。
ini
// 更新子元素的可见性
const updateChildrenVisibility = useCallback(() => {
// 如果limit有值,将可见子元素数量设置为limit,否则,根据容器的宽度和子元素的宽度计算可见子元素数量
if (limit !== undefined && limit !== null) {
setVisibleCount(limit);
} else {
if (shadowRef.current && containerRef.current) {
// 累加宽度初始化为0
let currentWidth = 0;
// 获取容器的最大宽度
const { width: maxWidth } =
containerRef.current.getBoundingClientRect();
const count = shadowRef.current.childElementCount;
const breadcrumbChildren = shadowRef.current.children;
let localCount = 0;
for (let i = 0; i < count; i++) {
const child = breadcrumbChildren[i];
const { width } = child.getBoundingClientRect();
// 如果当前宽度加上当前子元素的宽度超过了容器的最大宽度就结束循环
if (maxWidth - 12 <= currentWidth + width) {
break;
} else {
//否则,根据容器的宽度和子元素的宽度计算可见子元素数量
currentWidth += width;
localCount += 1;
}
}
setVisibleCount(localCount);
}
}
}, [limit]);
窗口尺寸变化的处理
使用 useEffect
钩子监听窗口大小的变化。当窗口大小改变时,调用 updateChildrenVisibility
函数来重新计算可见的子元素数量。这样确保了响应式行为,能适应窗口或容器宽度的变更。
scss
// 使用useEffect来处理窗口尺寸变化事件
useEffect(() => {
updateChildrenVisibility();
const handleResize = () => updateChildrenVisibility();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [children, limit]);
渲染可见和隐藏的子元素
组件使用 React.Children.toArray
将 children
转换为数组,并根据 visibleCount
状态来拆分为两个数组:visibleChildren
和 hiddenChildren
。visibleChildren
包含那些将会被直接渲染在容器中的子元素,对于 hiddenChildren
,组件使用 Dropdown
菜单来提供访问隐藏子元素的方式。当用户与 Dropdown
交互时(例如鼠标悬停),隐藏的子元素将作为下拉菜单项显示出来。
ini
const visibleChildren = React.Children.toArray(children).slice(
0,
visibleCount,
);
const hiddenChildren = React.Children.toArray(children).slice(visibleCount);
// 准备Dropdown菜单内容
const menu = (
<Menu>
{hiddenChildren.map((child, index) => (
<Menu.Item key={index}>{child}</Menu.Item>
))}
</Menu>
);
最终渲染
组件返回了一个包裹 wrapper
和 shadow
两部分的 JSX 结构。wrapper
用于显示可见的子元素,而 shadow
(虽然被设置为不可见)用于帮助计算子元素的宽度。Dropdown
菜单被放置在 wrapper
中,以便用户可以交互。
必须将
visibility: hidden
,而不是使用display: none
。当我们给一个元素设置display: none
时,这个元素会从文档流中完全消失。这意味着它不占据任何空间,也不会渲染任何内容,因此无法进行宽度测量
less
return (
<div ref={containerRef} style={{ position: 'relative', width: '100%' }}>
<div
className="wrapper"
style={{
display: 'flex',
overflow: 'hidden',
flexWrap: 'nowrap',
width: '100%',
}}
>
{visibleChildren.map((child, index) => (
<div key={index} style={{ display: 'inline-block' }}>
{child}
</div>
))}
{hiddenChildren.length > 0 && (
<Dropdown overlay={menu} trigger={['hover']} arrow>
<EllipsisOutlined style={{ fontSize: 16 }} />
</Dropdown>
)}
</div>
<div
className="shadow"
ref={shadowRef}
style={{
position: 'absolute',
zIndex: -1,
visibility: 'hidden',
flexWrap: 'nowrap',
display: 'flex',
}}
>
{children}
</div>
</div>
);
所有代码
ini
import { Dropdown, Menu } from 'antd';
import React, {
ReactNode,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { EllipsisOutlined } from '@ant-design/icons';
// 定义组件的Props类型
interface ResponsiveContainerProps {
children: ReactNode; // 期望传入任意的React节点作为children
limit?: number; // 可选属性,限制显示的子元素数量;如果未设置,将基于容器宽度动态决定
}
const ResponsiveContainer: React.FC<ResponsiveContainerProps> = ({
children,
limit,
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const [visibleCount, setVisibleCount] = useState(0);
const shadowRef = useRef<HTMLDivElement>(null);
// 更新子元素的可见性
const updateChildrenVisibility = useCallback(() => {
if (limit !== undefined && limit !== null) {
setVisibleCount(limit);
} else {
if (shadowRef.current && containerRef.current) {
let currentWidth = 0;
const { width: maxWidth } =
containerRef.current.getBoundingClientRect();
const count = shadowRef.current.childElementCount;
const breadcrumbChildren = shadowRef.current.children;
let localCount = 0;
for (let i = 0; i < count; i++) {
const child = breadcrumbChildren[i];
const { width } = child.getBoundingClientRect();
if (maxWidth - 12 <= currentWidth + width) {
break;
} else {
currentWidth += width;
localCount += 1;
}
}
setVisibleCount(localCount);
}
}
}, [limit]);
// 使用useEffect来处理窗口尺寸变化事件
useEffect(() => {
updateChildrenVisibility();
const handleResize = () => updateChildrenVisibility();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [children, limit]);
const visibleChildren = React.Children.toArray(children).slice(
0,
visibleCount,
);
const hiddenChildren = React.Children.toArray(children).slice(visibleCount);
// 准备Dropdown菜单内容
const menu = (
<Menu>
{hiddenChildren.map((child, index) => (
<Menu.Item key={index}>{child}</Menu.Item>
))}
</Menu>
);
return (
<div ref={containerRef} style={{ position: 'relative', width: '100%' }}>
<div
className="wrapper"
style={{
display: 'flex',
overflow: 'hidden',
flexWrap: 'nowrap',
width: '100%',
}}
>
{visibleChildren.map((child, index) => (
<div key={index} style={{ display: 'inline-block' }}>
{child}
</div>
))}
{hiddenChildren.length > 0 && (
<Dropdown overlay={menu} trigger={['hover']} arrow>
<EllipsisOutlined style={{ fontSize: 16 }} />
</Dropdown>
)}
</div>
<div
className="shadow"
ref={shadowRef}
style={{
position: 'absolute',
zIndex: -1,
visibility: 'hidden',
flexWrap: 'nowrap',
display: 'flex',
}}
>
{children}
</div>
</div>
);
};