npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kne/overflow-items

v0.1.0

Published

测量剩余宽度并自动决定露出若干 item 的 React 溢出收起库,提供组件、Hook 与纯函数,支持更多插槽与防抖。

Readme

overflow-items

描述

测量剩余宽度并自动决定露出若干 item 的 React 溢出收起库,提供组件、Hook 与纯函数,支持更多插槽与防抖。

安装

npm i --save @kne/overflow-items

概述

OverflowItems 帮你在有限宽度里「尽量多露、装不下就收起」。它测量容器剩余空间,预留「更多」按钮宽度与间距,自动算出能放出几个 item——按钮组、筛选条、标签行都可以复用同一套能力。

核心能力拆成三层,按需选用:纯函数 computeVisibleCount 做算法;useOverflowItems 负责测宽、Resize 防抖与稳定裁切;组件 OverflowItems 用 render props 接入你自己的 item / more UI,不绑 antd。表格等「列宽一致、item 多数相同」场景可用 OverflowItems.Share 共享测宽。

特点:

  • 首屏可先全量或最少占位,宽度稳定后再一次性裁切,减少闪烁
  • Resize 路径防抖(debounce 可配),拖拽窗口时不会疯狂重算
  • 支持 asc / desc 两种可见数策略,以及自定义 more 渲染
  • Headless 友好:业务只接插槽,Dropdown、展开收起等交互留在业务侧
  • OverflowItems.Share:同签名多实例只测一次,适合表格操作列

示例

示例代码

  • OverflowItems
  • 横向工具栏 / 筛选条 / 表格行操作等业务场景下,按容器宽度单行排布 item,装不下收入「更多」。覆盖 gap、debounce、beforeReady、strategy、renderMore(含 Dropdown)及 enabled 展开全部。
  • _OverflowItems(@kne/current-lib_overflow-items)[import * as _OverflowItems from "@kne/overflow-items"],(@kne/current-lib_overflow-items/dist/index.css),antd(antd),remoteLoader(@kne/remote-loader)
const { default: OverflowItems } = _OverflowItems;
const { createWithRemoteLoader } = remoteLoader;
const { Button, Card, Dropdown, Flex, Slider, Space, Switch, Tag, Typography } = antd;
const { Text } = Typography;
const { useState } = React;

/** 列表页操作栏按钮(业务场景) */
const LIST_TOOLBAR_ACTIONS = [
  { key: 'create', type: 'primary', children: '新建候选人' },
  { key: 'import', children: '批量导入' },
  { key: 'export', children: '导出 Excel' },
  { key: 'assign', children: '分配顾问' },
  { key: 'tag', children: '打标签' },
  { key: 'archive', children: '归档' },
  { key: 'delete', danger: true, children: '批量删除' }
];

/** 筛选条标签 */
const FILTER_TAGS = [
  { key: 'status', label: '状态:在职' },
  { key: 'city', label: '城市:上海 / 杭州' },
  { key: 'level', label: '职级:P6-P8' },
  { key: 'dept', label: '部门:技术研发中心' },
  { key: 'join', label: '入职:近 90 天' },
  { key: 'source', label: '来源:猎头推荐' },
  { key: 'owner', label: '负责人:张敏' }
];

/** 表格行操作 */
const ROW_ACTIONS = [
  { key: 'view', type: 'link', children: '查看' },
  { key: 'edit', type: 'link', children: '编辑' },
  { key: 'follow', type: 'link', children: '跟进' },
  { key: 'transfer', type: 'link', children: '转交' },
  { key: 'reject', type: 'link', children: '淘汰' },
  { key: 'delete', type: 'link', danger: true, children: '删除' }
];

const toolbarBox = width => ({
  width,
  maxWidth: '100%',
  padding: '8px 12px',
  background: '#fff',
  border: '1px solid #f0f0f0',
  borderRadius: 8
});

const BaseExample = createWithRemoteLoader({
  modules: ['components-core:Global@PureGlobal', 'components-core:InfoPage']
})(({ remoteModules }) => {
  const [PureGlobal, InfoPage] = remoteModules;
  const [toolbarWidth, setToolbarWidth] = useState(420);
  const [strategyWidth, setStrategyWidth] = useState(260);
  const [tagWidth, setTagWidth] = useState(320);
  const [rowWidth, setRowWidth] = useState(280);
  const [gap, setGap] = useState(8);
  const [debounce, setDebounce] = useState(100);
  const [beforeReadyMin, setBeforeReadyMin] = useState(false);
  const [expanded, setExpanded] = useState(false);

  return (
    <PureGlobal>
      <InfoPage>
        <InfoPage.Part title="列表工具栏(横向收起)" subtitle="拖动滑块改变容器宽度,按钮单行排布,装不下收入「更多」">
          <Space direction="vertical" size="middle" style={{ width: '100%', maxWidth: 640 }}>
            <Slider min={180} max={640} step={10} value={toolbarWidth} onChange={setToolbarWidth} tooltip={{ formatter: v => &#96;${v}px&#96; }} />
            <div style={toolbarBox(toolbarWidth)}>
              <OverflowItems
                items={LIST_TOOLBAR_ACTIONS}
                gap={gap}
                debounce={debounce}
                beforeReady={beforeReadyMin ? 'min' : 'all'}
                renderItem={item => <Button size="small" type={item.type} danger={item.danger}>{item.children}</Button>}
                renderMore={({ hiddenItems }) => (
                  <Dropdown
                    menu={{
                      items: hiddenItems.map(item => ({
                        key: item.key,
                        danger: item.danger,
                        label: item.children
                      }))
                    }}
                  >
                    <Button size="small">更多</Button>
                  </Dropdown>
                )}
              />
            </div>
            <Flex gap={16} wrap align="center">
              <Text type="secondary">间距 gap:</Text>
              <Slider min={0} max={16} style={{ width: 120 }} value={gap} onChange={setGap} />
              <Text type="secondary">防抖 debounce:</Text>
              <Slider min={0} max={300} step={50} style={{ width: 140 }} value={debounce} onChange={setDebounce} />
              <Text type="secondary">beforeReady=min</Text>
              <Switch checked={beforeReadyMin} onChange={setBeforeReadyMin} />
            </Flex>
          </Space>
        </InfoPage.Part>

        <InfoPage.Part title="strategy 左右对比" subtitle="同一宽度下并排对比 asc / desc,观察不等宽 Tag 的横向可见数量">
          <Space direction="vertical" size="middle" style={{ width: '100%' }}>
            <Slider min={160} max={400} step={10} value={strategyWidth} onChange={setStrategyWidth} tooltip={{ formatter: v => &#96;${v}px&#96; }} style={{ maxWidth: 480 }} />
            <Flex gap={16} wrap="nowrap">
              <Card size="small" title="asc · 从少往多" style={{ width: strategyWidth, flex: 'none' }}>
                <OverflowItems
                  items={FILTER_TAGS}
                  strategy="asc"
                  gap={8}
                  renderItem={item => <Tag>{item.label}</Tag>}
                  renderMore={() => <Tag>更多</Tag>}
                />
              </Card>
              <Card size="small" title="desc · 从多往少" style={{ width: strategyWidth, flex: 'none' }}>
                <OverflowItems
                  items={FILTER_TAGS}
                  strategy="desc"
                  gap={8}
                  renderItem={item => <Tag>{item.label}</Tag>}
                  renderMore={() => <Tag>更多</Tag>}
                />
              </Card>
            </Flex>
          </Space>
        </InfoPage.Part>

        <InfoPage.Part title="自定义 renderMore" subtitle="筛选条场景:+N 下拉查看隐藏条件,也可临时关闭裁切展开全部">
          <Space direction="vertical" size="middle" style={{ width: '100%', maxWidth: 560 }}>
            <Flex align="center" gap={12}>
              <Slider min={180} max={560} step={10} value={tagWidth} onChange={setTagWidth} tooltip={{ formatter: v => &#96;${v}px&#96; }} style={{ flex: 1 }} />
              <Button size="small" type="link" onClick={() => setExpanded(v => !v)}>
                {expanded ? '恢复自适应' : '展开全部'}
              </Button>
            </Flex>
            <div style={toolbarBox(tagWidth)}>
              <OverflowItems
                items={FILTER_TAGS}
                gap={8}
                enabled={!expanded}
                renderItem={item => <Tag color="blue">{item.label}</Tag>}
                renderMore={({ hiddenItems, hiddenCount }) => (
                  <Dropdown
                    menu={{
                      items: hiddenItems.map(item => ({
                        key: item.key,
                        label: item.label
                      }))
                    }}
                  >
                    <Tag style={{ cursor: 'pointer' }}>+{hiddenCount}</Tag>
                  </Dropdown>
                )}
              />
            </div>
          </Space>
        </InfoPage.Part>

        <InfoPage.Part title="表格行操作(link)" subtitle="窄列场景下横向 link 按钮自动收起">
          <Space direction="vertical" size="middle" style={{ width: '100%', maxWidth: 480 }}>
            <Slider min={120} max={480} step={10} value={rowWidth} onChange={setRowWidth} tooltip={{ formatter: v => &#96;${v}px&#96; }} />
            <div style={toolbarBox(rowWidth)}>
              <OverflowItems
                items={ROW_ACTIONS}
                gap={0}
                beforeReady="min"
                renderItem={item => (
                  <Button type="link" size="small" danger={item.danger}>
                    {item.children}
                  </Button>
                )}
                renderMore={({ hiddenItems }) => (
                  <Dropdown
                    menu={{
                      items: hiddenItems.map(item => ({
                        key: item.key,
                        danger: item.danger,
                        label: item.children
                      }))
                    }}
                  >
                    <Button type="link" size="small">
                      更多
                    </Button>
                  </Dropdown>
                )}
              />
            </div>
          </Space>
        </InfoPage.Part>
      </InfoPage>
    </PureGlobal>
  );
});

render(<BaseExample />);
  • OverflowItems.Share
  • 定高滚动列表(默认 100 行可调):开关 Share 对比测量层 DOM 数与挂载耗时;拖宽度观察同桶同步裁切与卡顿差异。
  • _OverflowItems(@kne/current-lib_overflow-items)[import * as _OverflowItems from "@kne/overflow-items"],(@kne/current-lib_overflow-items/dist/index.css),antd(antd),remoteLoader(@kne/remote-loader)
const { default: OverflowItems, useOverflowItems } = _OverflowItems;
const { createWithRemoteLoader } = remoteLoader;
const { Alert, Button, Card, Dropdown, Flex, Slider, Space, Switch, Tag, Typography } = antd;
const { Text, Paragraph } = Typography;
const { useEffect, useMemo, useState } = React;

const ROW_ACTIONS = [
  { key: 'view', children: '查看' },
  { key: 'edit', children: '编辑' },
  { key: 'follow', children: '跟进' },
  { key: 'transfer', children: '转交' },
  { key: 'remark', children: '备注' },
  { key: 'assign', children: '分配' },
  { key: 'reject', children: '淘汰' },
  { key: 'delete', children: '删除', danger: true }
];

const ADMIN_ACTIONS = [
  { key: 'view', children: '查看' },
  { key: 'edit', children: '编辑' },
  { key: 'audit', children: '审核' },
  { key: 'export', children: '导出' },
  { key: 'permission', children: '权限' },
  { key: 'delete', children: '删除', danger: true }
];

const MEASURE_CLASS = 'demo-share-measure';
const LIST_HEIGHT = 360;
const ROW_HEIGHT = 40;

const roleTag = role => {
  if (role === 'leader') {
    return <Tag color="blue">Leader</Tag>;
  }
  if (role === 'follower') {
    return <Tag color="green">Follower</Tag>;
  }
  return <Tag>独立</Tag>;
};

/** 用 Hook 搭一行,便于展示 role / shouldMeasure */
const ActionRow = ({ items, shareKey, width, gap = 0 }) => {
  const { setContainerRef, setMeasureRef, setMoreMeasureRef, visibleCount, ready, hiddenCount, role, shouldMeasure } = useOverflowItems({
    itemCount: items.length,
    items,
    shareKey,
    gap,
    beforeReady: 'min',
    debounce: 80
  });

  const visibleItems = items.slice(0, visibleCount);
  const hiddenItems = items.slice(visibleCount);

  return (
    <Flex align="center" gap={8} style={{ width: '100%', height: ROW_HEIGHT }}>
      <div style={{ width, maxWidth: '100%', minWidth: 0, flex: 'none', border: '1px dashed #d9d9d9', borderRadius: 6, padding: '2px 8px', background: '#fafafa' }}>
        <div style={{ position: 'relative' }}>
          {shouldMeasure ? (
            <div
              ref={setMeasureRef}
              className={MEASURE_CLASS}
              aria-hidden
              style={{
                position: 'absolute',
                visibility: 'hidden',
                height: 0,
                overflow: 'hidden',
                display: 'flex',
                flexWrap: 'nowrap',
                alignItems: 'center',
                gap,
                pointerEvents: 'none'
              }}
            >
              {items.map(item => (
                <div key={item.key} data-overflow-item>
                  <Button type="link" size="small" danger={item.danger}>
                    {item.children}
                  </Button>
                </div>
              ))}
              <div ref={setMoreMeasureRef} data-overflow-more>
                <Button type="link" size="small">
                  更多
                </Button>
              </div>
            </div>
          ) : null}
          <div
            ref={setContainerRef}
            style={{
              display: 'flex',
              flexWrap: 'nowrap',
              alignItems: 'center',
              gap,
              overflow: 'hidden',
              width: '100%'
            }}
          >
            {visibleItems.map(item => (
              <Button key={item.key} type="link" size="small" danger={item.danger} style={{ flexShrink: 0, paddingInline: 4 }}>
                {item.children}
              </Button>
            ))}
            {ready && hiddenCount > 0 ? (
              <Dropdown
                menu={{
                  items: hiddenItems.map(item => ({
                    key: item.key,
                    danger: item.danger,
                    label: item.children
                  }))
                }}
              >
                <Button type="link" size="small" style={{ flexShrink: 0, paddingInline: 4 }}>
                  更多
                </Button>
              </Dropdown>
            ) : null}
          </div>
        </div>
      </div>
      <Space size={4} wrap>
        {roleTag(role)}
        <Tag color={shouldMeasure ? 'orange' : 'default'} style={{ marginInlineEnd: 0 }}>
          {shouldMeasure ? '测' : '无测'}
        </Tag>
        <Text type="secondary" style={{ fontSize: 12 }}>
          {visibleCount}/{items.length}
        </Text>
      </Space>
    </Flex>
  );
};

const RowList = ({ rows, width, enableShare, listKey }) => {
  const body = (
    <div key={listKey}>
      {rows.map(row => {
        const actions = row.isAdmin ? ADMIN_ACTIONS : ROW_ACTIONS;
        return (
          <Flex
            key={row.id}
            align="center"
            gap={12}
            style={{
              height: ROW_HEIGHT,
              paddingInline: 8,
              borderBottom: '1px solid #f0f0f0',
              boxSizing: 'border-box'
            }}
          >
            <Text style={{ width: 120, flex: 'none' }} ellipsis>
              {row.name}
            </Text>
            <ActionRow items={actions} shareKey={row.isAdmin ? 'actions-admin' : undefined} width={width} />
          </Flex>
        );
      })}
    </div>
  );

  return enableShare ? <OverflowItems.Share>{body}</OverflowItems.Share> : body;
};

const BaseExample = createWithRemoteLoader({
  modules: ['components-core:Global@PureGlobal', 'components-core:InfoPage']
})(({ remoteModules }) => {
  const [PureGlobal, InfoPage] = remoteModules;
  const [colWidth, setColWidth] = useState(200);
  const [rowCount, setRowCount] = useState(100);
  const [enableShare, setEnableShare] = useState(true);
  const [measureCount, setMeasureCount] = useState(0);
  const [mountMs, setMountMs] = useState(null);
  const [remountToken, setRemountToken] = useState(0);

  const rows = useMemo(
    () =>
      Array.from({ length: rowCount }, (_, index) => {
        const isAdmin = index === rowCount - 1;
        return {
          id: index + 1,
          name: isAdmin ? '管理员(独立桶)' : &#96;候选人 ${index + 1}&#96;,
          isAdmin
        };
      }),
    [rowCount]
  );

  const expectedBuckets = 2;

  // 切换 Share / 行数时强制整表重挂,用 rAF 粗测挂载耗时
  useEffect(() => {
    const start = performance.now();
    let raf2 = 0;
    const raf1 = requestAnimationFrame(() => {
      raf2 = requestAnimationFrame(() => {
        setMountMs(Math.round(performance.now() - start));
      });
    });
    return () => {
      cancelAnimationFrame(raf1);
      cancelAnimationFrame(raf2);
    };
  }, [enableShare, rowCount, remountToken]);

  useEffect(() => {
    const sync = () => {
      setMeasureCount(document.querySelectorAll(&#96;.${MEASURE_CLASS}&#96;).length);
    };
    sync();
    const id = window.setInterval(sync, 300);
    return () => window.clearInterval(id);
  }, [enableShare, colWidth, rowCount, remountToken]);

  const onToggleShare = checked => {
    setEnableShare(checked);
    setRemountToken(v => v + 1);
  };

  return (
    <PureGlobal>
      <InfoPage>
        <InfoPage.Part
          title="OverflowItems.Share 共享测宽"
          subtitle="定高滚动列表:调行数、开关 Share,对比测量层数量与挂载耗时;拖动宽度看同桶同步裁切。"
        >
          <Space direction="vertical" size="middle" style={{ width: '100%', maxWidth: 920 }}>
            <Alert
              type="info"
              showIcon
              message="怎么测性能差距?"
              description={
                <ul style={{ margin: '8px 0 0', paddingLeft: 18 }}>
                  <li>
                    把行数拉到 100+,开关 <Text code>Share</Text>:看「测量层 DOM 数」和「挂载耗时」。
                  </li>
                  <li>
                    开 Share:测量层 ≈ 2(普通操作桶 + 管理员桶);关 Share:测量层 ≈ 行数。
                  </li>
                  <li>列表定高 {LIST_HEIGHT}px 可滚动;拖操作区宽度时,关 Share 更容易感到卡顿。</li>
                </ul>
              }
            />

            <Card size="small">
              <Flex wrap gap={24} align="center">
                <Space>
                  <Text type="secondary">启用 Share</Text>
                  <Switch checked={enableShare} onChange={onToggleShare} checkedChildren="开" unCheckedChildren="关" />
                </Space>
                <Space>
                  <Text type="secondary">行数</Text>
                  <Slider min={20} max={300} step={20} value={rowCount} onChange={setRowCount} style={{ width: 180 }} />
                  <Text type="secondary">{rowCount}</Text>
                </Space>
                <Space>
                  <Text type="secondary">操作区宽度</Text>
                  <Slider min={100} max={360} step={10} value={colWidth} onChange={setColWidth} style={{ width: 180 }} tooltip={{ formatter: v => &#96;${v}px&#96; }} />
                  <Text type="secondary">{colWidth}px</Text>
                </Space>
                <Button
                  size="small"
                  onClick={() => {
                    setRemountToken(v => v + 1);
                  }}
                >
                  重新挂载计时
                </Button>
              </Flex>
            </Card>

            <Flex gap={16} wrap>
              <Card size="small" style={{ minWidth: 140 }}>
                <Text type="secondary">测量层 DOM</Text>
                <div>
                  <Text strong style={{ fontSize: 28, color: enableShare ? '#1677ff' : '#fa8c16' }}>
                    {measureCount}
                  </Text>
                </div>
                <Text type="secondary" style={{ fontSize: 12 }}>
                  {enableShare ? &#96;期望 ≈ ${expectedBuckets}&#96; : &#96;期望 ≈ ${rowCount}&#96;}
                </Text>
              </Card>
              <Card size="small" style={{ minWidth: 140 }}>
                <Text type="secondary">挂载耗时(约)</Text>
                <div>
                  <Text strong style={{ fontSize: 28 }}>
                    {mountMs == null ? '—' : mountMs}
                  </Text>
                  <Text type="secondary"> ms</Text>
                </div>
                <Text type="secondary" style={{ fontSize: 12 }}>
                  切换 Share / 行数 / 点重挂载后更新
                </Text>
              </Card>
              <Card size="small" style={{ flex: 1, minWidth: 220 }}>
                <Paragraph style={{ marginBottom: 0 }} type="secondary">
                  {enableShare
                    ? &#96;共享模式:${rowCount} 行里绝大多数是 Follower,只渲染可见按钮,测宽成本接近常数。&#96;
                    : &#96;独立模式:${rowCount} 行各挂一套测量 DOM + ResizeObserver,行数上去后拖宽度更容易卡。&#96;}
                </Paragraph>
              </Card>
            </Flex>

            <Card
              size="small"
              title={enableShare ? &#96;Share 开 · ${rowCount} 行&#96; : &#96;Share 关 · ${rowCount} 行独立测宽&#96;}
              styles={{ body: { padding: 0 } }}
            >
              <div style={{ height: LIST_HEIGHT, overflow: 'auto' }}>
                <RowList rows={rows} width={colWidth} enableShare={enableShare} listKey={&#96;${enableShare}-${rowCount}-${remountToken}&#96;} />
              </div>
            </Card>
          </Space>
        </InfoPage.Part>
      </InfoPage>
    </PureGlobal>
  );
});

render(<BaseExample />);
  • useOverflowItems
  • 无 UI 测宽 Hook:自行搭建测量层与可见层 DOM,获取 visibleCount / ready / hiddenCount。示例模拟候选人详情页操作条,并展示测量态与可见数量。
  • _OverflowItems(@kne/current-lib_overflow-items)[import * as _OverflowItems from "@kne/overflow-items"],(@kne/current-lib_overflow-items/dist/index.css),antd(antd),remoteLoader(@kne/remote-loader)
const { useOverflowItems } = _OverflowItems;
const { createWithRemoteLoader } = remoteLoader;
const { Button, Dropdown, Flex, Slider, Space, Tag, Typography } = antd;
const { Text } = Typography;
const { useMemo, useState } = React;

const CANDIDATE_ACTIONS = [
  { key: 'resume', label: '查看简历' },
  { key: 'interview', label: '安排面试' },
  { key: 'offer', label: '发送 Offer' },
  { key: 'remark', label: '添加备注' },
  { key: 'share', label: '分享给同事' },
  { key: 'blacklist', label: '加入黑名单' }
];

const HeadlessToolbar = ({ items, width, gap = 8 }) => {
  const { setContainerRef, setMeasureRef, setMoreMeasureRef, visibleCount, ready, hiddenCount } = useOverflowItems({
    itemCount: items.length,
    gap,
    debounce: 100,
    beforeReady: 'all'
  });

  const visibleItems = items.slice(0, visibleCount);
  const hiddenItems = items.slice(visibleCount);

  return (
    <div style={{ width, maxWidth: '100%', padding: '8px 12px', background: '#fff', border: '1px solid #f0f0f0', borderRadius: 8 }}>
      <div style={{ position: 'relative' }}>
        <div
          ref={setMeasureRef}
          aria-hidden
          style={{
            position: 'absolute',
            visibility: 'hidden',
            height: 0,
            overflow: 'hidden',
            display: 'flex',
            flexDirection: 'row',
            flexWrap: 'nowrap',
            alignItems: 'center',
            gap,
            pointerEvents: 'none'
          }}
        >
          {items.map(item => (
            <div key={item.key} data-overflow-item>
              <Button size="small">{item.label}</Button>
            </div>
          ))}
          <div ref={setMoreMeasureRef} data-overflow-more>
            <Button size="small">更多</Button>
          </div>
        </div>
        <div
          ref={setContainerRef}
          style={{
            display: 'flex',
            flexDirection: 'row',
            flexWrap: 'nowrap',
            alignItems: 'center',
            gap,
            overflow: 'hidden',
            width: '100%'
          }}
        >
          {visibleItems.map(item => (
            <Button key={item.key} size="small" style={{ flexShrink: 0 }}>
              {item.label}
            </Button>
          ))}
          {ready && hiddenCount > 0 ? (
            <Dropdown
              menu={{
                items: hiddenItems.map(item => ({
                  key: item.key,
                  label: item.label
                }))
              }}
            >
              <Button size="small" style={{ flexShrink: 0 }}>
                更多
              </Button>
            </Dropdown>
          ) : null}
        </div>
      </div>
      <Flex gap={8} style={{ marginTop: 8 }}>
        <Tag color={ready ? 'success' : 'processing'}>{ready ? 'ready' : 'measuring'}</Tag>
        <Text type="secondary">
          visibleCount={visibleCount} / hiddenCount={hiddenCount}
        </Text>
      </Flex>
    </div>
  );
};

const BaseExample = createWithRemoteLoader({
  modules: ['components-core:Global@PureGlobal', 'components-core:InfoPage']
})(({ remoteModules }) => {
  const [PureGlobal, InfoPage] = remoteModules;
  const [width, setWidth] = useState(360);
  const items = useMemo(() => CANDIDATE_ACTIONS, []);

  return (
    <PureGlobal>
      <InfoPage>
        <InfoPage.Part
          title="useOverflowItems 无 UI 测宽"
          subtitle="自行搭建测量层与可见层 DOM,Hook 只返回 visibleCount / ready;适合已有布局不想用 OverflowItems 组件的场景"
        >
          <Space direction="vertical" size="middle" style={{ width: '100%', maxWidth: 560 }}>
            <Text type="secondary">候选人详情页操作条:拖动宽度观察 Hook 输出的可见数量</Text>
            <Slider min={160} max={560} step={10} value={width} onChange={setWidth} tooltip={{ formatter: v => &#96;${v}px&#96; }} />
            <HeadlessToolbar items={items} width={width} gap={8} />
          </Space>
        </InfoPage.Part>
      </InfoPage>
    </PureGlobal>
  );
});

render(<BaseExample />);
  • computeVisibleCount
  • 纯函数计算可见数量:传入容器宽、itemWidths、moreWidth、gap、safetyGap 与 strategy,对比 asc / desc 结果,便于在非 React 环境或单测中复用算法。
  • _OverflowItems(@kne/current-lib_overflow-items)[import * as _OverflowItems from "@kne/overflow-items"],antd(antd),remoteLoader(@kne/remote-loader)
const { computeVisibleCount } = _OverflowItems;
const { createWithRemoteLoader } = remoteLoader;
const { Button, Card, Flex, InputNumber, Space, Table, Typography } = antd;
const { Text } = Typography;
const { useMemo, useState } = React;

const BaseExample = createWithRemoteLoader({
  modules: ['components-core:Global@PureGlobal', 'components-core:InfoPage']
})(({ remoteModules }) => {
  const [PureGlobal, InfoPage] = remoteModules;
  const [containerWidth, setContainerWidth] = useState(280);
  const [moreWidth, setMoreWidth] = useState(64);
  const [gap, setGap] = useState(8);
  const [safetyGap, setSafetyGap] = useState(2);
  const [itemWidths, setItemWidths] = useState([72, 64, 88, 96, 80, 72]);

  const ascCount = useMemo(
    () =>
      computeVisibleCount({
        containerWidth,
        itemWidths,
        moreWidth,
        gap,
        safetyGap,
        strategy: 'asc'
      }),
    [containerWidth, itemWidths, moreWidth, gap, safetyGap]
  );

  const descCount = useMemo(
    () =>
      computeVisibleCount({
        containerWidth,
        itemWidths,
        moreWidth,
        gap,
        safetyGap,
        strategy: 'desc'
      }),
    [containerWidth, itemWidths, moreWidth, gap, safetyGap]
  );

  const previewRows = itemWidths.map((w, index) => ({
    key: index,
    index: index + 1,
    width: w,
    ascVisible: index < ascCount,
    descVisible: index < descCount
  }));

  return (
    <PureGlobal>
      <InfoPage>
        <InfoPage.Part
          title="computeVisibleCount 纯函数"
          subtitle="无 DOM:给定容器宽、各 item 宽、更多按钮宽与间距,直接算出可见数量;可在非 React 环境复用"
        >
          <Space direction="vertical" size="middle" style={{ width: '100%', maxWidth: 720 }}>
            <Flex wrap gap={16}>
              <Space>
                <Text type="secondary">容器宽度</Text>
                <InputNumber min={80} max={800} value={containerWidth} onChange={setContainerWidth} addonAfter="px" />
              </Space>
              <Space>
                <Text type="secondary">更多按钮宽</Text>
                <InputNumber min={0} max={200} value={moreWidth} onChange={setMoreWidth} addonAfter="px" />
              </Space>
              <Space>
                <Text type="secondary">gap</Text>
                <InputNumber min={0} max={24} value={gap} onChange={setGap} />
              </Space>
              <Space>
                <Text type="secondary">safetyGap</Text>
                <InputNumber min={0} max={16} value={safetyGap} onChange={setSafetyGap} />
              </Space>
            </Flex>

            <Card size="small" title="itemWidths(模拟按钮实测宽度)">
              <Space wrap>
                {itemWidths.map((w, index) => (
                  <InputNumber
                    key={index}
                    min={24}
                    max={200}
                    value={w}
                    onChange={value => {
                      setItemWidths(list => list.map((item, i) => (i === index ? value || 24 : item)));
                    }}
                    addonBefore={&#96;#${index + 1}&#96;}
                    addonAfter="px"
                  />
                ))}
                <Button
                  onClick={() => setItemWidths(list => [...list, 72])}
                  disabled={itemWidths.length >= 10}
                >
                  增加 item
                </Button>
                <Button
                  onClick={() => setItemWidths(list => list.slice(0, -1))}
                  disabled={itemWidths.length <= 1}
                >
                  减少 item
                </Button>
              </Space>
            </Card>

            <Flex gap={24}>
              <Card size="small" title="strategy=asc">
                <Text strong style={{ fontSize: 24 }}>
                  {ascCount}
                </Text>
                <Text type="secondary"> 个可见</Text>
              </Card>
              <Card size="small" title="strategy=desc">
                <Text strong style={{ fontSize: 24 }}>
                  {descCount}
                </Text>
                <Text type="secondary"> 个可见</Text>
              </Card>
            </Flex>

            <Table
              size="small"
              pagination={false}
              dataSource={previewRows}
              columns={[
                { title: '序号', dataIndex: 'index', width: 64 },
                { title: '宽度', dataIndex: 'width', width: 96, render: v => &#96;${v}px&#96; },
                {
                  title: 'asc 是否露出',
                  dataIndex: 'ascVisible',
                  render: v => (v ? '是' : '否')
                },
                {
                  title: 'desc 是否露出',
                  dataIndex: 'descVisible',
                  render: v => (v ? '是' : '否')
                }
              ]}
            />
          </Space>
        </InfoPage.Part>
      </InfoPage>
    </PureGlobal>
  );
});

render(<BaseExample />);

API

OverflowItems

根据容器宽度自动决定露出多少 item,并将剩余项收起到「更多」。无 UI 框架依赖,通过 renderItem / renderMore 自定义展示。

属性

| 属性 | 类型 | 默认值 | 说明 | | ---------------- | --------------------------------- | ------------------------ | ---------------------------------------------------------------------------------------------- | | items | any[] | [] | 数据源 | | renderItem | (item, index, ctx) => ReactNode | — | 渲染单个 item;ctx 含 visible、ready | | renderMore | (ctx) => ReactNode | 默认文案「更多」 | 渲染收起入口;ctx 含 hiddenItems、hiddenCount、visibleCount、ready;无隐藏项时不渲染 | | gap | number | 8 | item 间距(同时用于测量计算与可见层 CSS gap) | | strategy | 'asc' \| 'desc' | 'asc' | 可见数计算策略:asc 从少往多,desc 从多往少 | | enabled | boolean | true | 为 false 时展示全部,不裁切 | | beforeReady | 'all' \| 'min' | 'all' | 首次测量完成前:all 先全量,min 至少保留 1 个并隐藏可见区防闪 | | debounce | number | 100 | Resize 触发重算的防抖毫秒;0 关闭防抖 | | safetyGap | number | 2 | 计算时的宽度容差 | | expandHysteresis | number | 16 | ready 后变宽再放出更多项的滞后像素 | | settle | number | 100 | 首次裁切前容器宽度需连续稳定的时长 | | maxSettle | number | 400 | 测量态最长等待,超时强制结束 | | itemSelector | string | '[data-overflow-item]' | 测量层 item 选择器 | | observeParent | boolean | true | 是否同时观察父节点宽度变化 | | shareKey | string | — | 覆盖 Share 分桶签名;不传则从 items 的 key/id/label/children 推导 | | className | string | — | 根节点 class | | measureClassName | string | — | 测量层 class | | contentClassName | string | — | 可见层 class | | style | CSSProperties | — | 根节点行内样式 |

OverflowItems.Share

共享测宽上下文。包在表格等「同列宽、item 多数相同」场景外,组内同签名实例只保留一个 Leader 测宽,其余 Follower 不渲染测量层、不挂 ResizeObserver,只消费共享的 visibleCount / ready。

<OverflowItems.Share>
  {rows.map(row => (
    <OverflowItems key={row.id} items={row.actions} renderItem={...} renderMore={...} />
  ))}
</OverflowItems.Share>

宽度约定:同 bucket 内各实例容器宽度须一致(如表格固定操作列)。行宽真不同时不要包进同一 Share,或使用不同 shareKey。

分桶:默认由 items 标识 + gap / strategy / safetyGap / expandHysteresis / itemCount 组成签名;可用 shareKey 覆盖。Leader 卸载后按注册顺序提升下一个成员。也可 import { OverflowItemsShare } from '@kne/overflow-items'。

useOverflowItems

无 UI 的测宽 Hook:绑定容器 / 测量层 / more 测量节点后,返回可见数量。Resize 走防抖,配置或 itemCount 变化立即重算。处于 OverflowItems.Share 内时自动参与共享。

参数

| 参数 | 类型 | 默认值 | 说明 | | ---------------- | ----------------- | ------------------------ | --------------------------------------------------- | | itemCount | number | 0 | item 总数 | | items | any[] | — | 用于 Share 签名推导;组件场景会传入 | | shareKey | string | — | 覆盖 Share 分桶签名 | | enabled | boolean | true | 关闭时 visibleCount = itemCount 且 ready = true | | strategy | 'asc' \| 'desc' | 'asc' | 传给 computeVisibleCount | | gap | number | 8 | 间距 | | safetyGap | number | 2 | 计算容差 | | expandHysteresis | number | 16 | 变宽放出滞后 | | settle | number | 100 | 首次裁切前稳定时长 | | maxSettle | number | 400 | 强制结束测量态 | | debounce | number | 100 | Resize 防抖;0 关闭 | | beforeReady | 'all' \| 'min' | 'all' | 未 ready 时的占位策略 | | itemSelector | string | '[data-overflow-item]' | 测量层 item 选择器 | | observeParent | boolean | true | 是否观察父节点 |

返回值

| 字段 | 类型 | 说明 | | ----------------- | ------------------------------ | ------------------------------------------------------------- | | setContainerRef | (node) => void | 可见容器 / 宽度基准 | | setMeasureRef | (node) => void | 隐藏测量层(需包含全量 item) | | setMoreMeasureRef | (node) => void | 「更多」测量节点 | | visibleCount | number | 当前应露出的数量 | | ready | boolean | 是否已完成首次稳定裁切 | | hiddenCount | number | itemCount - visibleCount | | role | 'leader' \| 'follower' \| null | Share 内角色;未共享时为 null | | shouldMeasure | boolean | 是否需要渲染测量层(Follower 为 false) |

测量 DOM 约定

测量层内每个 item 需带 data-overflow-item(或与 itemSelector 一致);「更多」节点通过 setMoreMeasureRef 绑定。shouldMeasure === false 时可省略测量层。

computeVisibleCount

纯函数:根据容器与各段宽度计算可见 item 数,无 DOM / React 依赖。

参数

| 参数 | 类型 | 默认值 | 说明 | | -------------- | ----------------- | ------- | ------------------------------ | | containerWidth | number | — | 容器可用宽度 | | itemWidths | number[] | — | 各 item 宽度 | | moreWidth | number | — | 「更多」按钮宽度(收起时预留) | | gap | number | 8 | item 间距 | | strategy | 'asc' \| 'desc' | 'asc' | 推算方向 | | safetyGap | number | 2 | 宽度容差 |

返回值

| 类型 | 说明 | | ------ | -------------------------------------------------- | | number | 可见 item 数量;有 item 时至少为 1;无 item 时为 0 |