@sia.soul/sia-react-ui 0.1.38 → 0.1.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -190,6 +190,7 @@ function Tag({
190
190
  // src/components/Tabs.tsx
191
191
  import { useEffect, useId, useLayoutEffect as useLayoutEffect2, useMemo, useRef as useRef2, useState as useState3 } from "react";
192
192
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
193
+ import { createElement } from "react";
193
194
  function Tabs({
194
195
  items,
195
196
  activeKey,
@@ -224,6 +225,42 @@ function Tabs({
224
225
  const sliding = ["capsule", "line", "underline", "tech", "tech-line"].includes(type);
225
226
  const listRef = useRef2(null);
226
227
  const indicatorRef = useRef2(null);
228
+ const moreRef = useRef2(null);
229
+ const [overflow, setOverflow] = useState3({ count: items.length, width: 0 });
230
+ const [moreOpen, setMoreOpen] = useState3(false);
231
+ const visibleCount = resolvedOrientation === "horizontal" ? overflow.count : items.length;
232
+ const hiddenItems = items.slice(visibleCount);
233
+ useLayoutEffect2(() => {
234
+ const list = listRef.current;
235
+ if (!list || resolvedOrientation !== "horizontal") return;
236
+ const entries = Array.from(list.children).filter((node) => node instanceof HTMLElement && node.hasAttribute("data-tab-entry"));
237
+ const measure = () => {
238
+ const css = getComputedStyle(list);
239
+ const gap = parseFloat(css.columnGap) || 0;
240
+ const padding = (parseFloat(css.paddingLeft) || 0) + (parseFloat(css.paddingRight) || 0);
241
+ const widths = entries.map((node) => node.getBoundingClientRect().width);
242
+ const total = widths.reduce((sum, width2) => sum + width2, 0) + gap * Math.max(0, widths.length - 1);
243
+ const available = Math.max(0, list.clientWidth - padding);
244
+ let count = widths.length;
245
+ if (total > available + 0.5) {
246
+ const moreWidth = moreRef.current?.getBoundingClientRect().width ?? 36;
247
+ let used = moreWidth;
248
+ count = 0;
249
+ for (const width2 of widths) {
250
+ if (used + gap + width2 > available + 0.5) break;
251
+ used += gap + width2;
252
+ count++;
253
+ }
254
+ }
255
+ const width = Math.ceil(total + padding + (parseFloat(css.borderLeftWidth) || 0) + (parseFloat(css.borderRightWidth) || 0));
256
+ setOverflow((previous) => previous.count === count && previous.width === width ? previous : { count, width });
257
+ };
258
+ measure();
259
+ const observer = new ResizeObserver(measure);
260
+ observer.observe(list);
261
+ entries.forEach((entry) => observer.observe(entry));
262
+ return () => observer.disconnect();
263
+ }, [items, type, size, closable, resolvedOrientation, tabBarTitle, tabBarExtraContent]);
227
264
  const [contentState, setContentState] = useState3(() => ({ key: selectedKey, visited: /* @__PURE__ */ new Set([selectedKey]) }));
228
265
  const settledKeyRef = useRef2(selectedKey);
229
266
  const afterChangeRef = useRef2(onAfterChange);
@@ -238,13 +275,14 @@ function Tabs({
238
275
  function updateIndicator() {
239
276
  if (!list || !indicator) return;
240
277
  const active = list.querySelector('[role="tab"][aria-selected="true"]');
241
- if (!active) {
278
+ if (!active || active.closest('[data-tab-hidden="true"]')) {
242
279
  indicator.style.visibility = "hidden";
243
280
  return;
244
281
  }
245
282
  const listRect = list.getBoundingClientRect();
246
283
  const rect = active.getBoundingClientRect();
247
- indicator.style.transform = `translate(${rect.left - listRect.left - list.clientLeft + list.scrollLeft}px, ${rect.top - listRect.top - list.clientTop + list.scrollTop}px)`;
284
+ const currentRect = active.getBoundingClientRect();
285
+ indicator.style.transform = `translate(${currentRect.left - listRect.left - list.clientLeft + list.scrollLeft}px, ${currentRect.top - listRect.top - list.clientTop + list.scrollTop}px)`;
248
286
  indicator.style.width = `${rect.width}px`;
249
287
  indicator.style.height = `${rect.height}px`;
250
288
  indicator.style.visibility = "visible";
@@ -258,7 +296,7 @@ function Tabs({
258
296
  observer.observe(list);
259
297
  list.querySelectorAll('[role="tab"]').forEach((tab) => observer.observe(tab));
260
298
  return () => observer.disconnect();
261
- }, [sliding, type, selectedKey, items, size, resolvedPosition, centered, closable]);
299
+ }, [sliding, type, selectedKey, items, size, resolvedPosition, centered, closable, visibleCount]);
262
300
  useEffect(() => {
263
301
  let cancelled = false;
264
302
  let frame = 0;
@@ -292,7 +330,8 @@ function Tabs({
292
330
  }
293
331
  function focusAndSelect(key) {
294
332
  selectTab(key);
295
- document.getElementById(`${baseId}-tab-${key}`)?.focus();
333
+ if (items.findIndex((item) => item.key === key) >= visibleCount) moreRef.current?.focus();
334
+ else document.getElementById(`${baseId}-tab-${key}`)?.focus();
296
335
  }
297
336
  function handleKeyDown(event, key) {
298
337
  const previousKey = resolvedOrientation === "horizontal" ? "ArrowLeft" : "ArrowUp";
@@ -318,49 +357,49 @@ function Tabs({
318
357
  tabBarTitle,
319
358
  type === "tech-line" ? /* @__PURE__ */ jsx4("svg", { className: "sia-tabs__title-connector", viewBox: "0 0 32 40", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("path", { d: "M0 39.25 H5 Q8 39.25 10 35.25 L28 3.75 Q29 1.75 32 1.75", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinejoin: "round" }) }) : null
320
359
  ] }) : null,
321
- /* @__PURE__ */ jsxs4("div", { ref: listRef, className: `sia-tabs__list${sliding ? " sia-tabs__list--sliding" : ""}`, role: "tablist", "aria-orientation": resolvedOrientation, children: [
360
+ /* @__PURE__ */ jsxs4("div", { ref: listRef, style: resolvedOrientation === "horizontal" && overflow.width ? { width: overflow.width } : void 0, className: `sia-tabs__list${sliding ? " sia-tabs__list--sliding" : ""}`, role: "tablist", "aria-orientation": resolvedOrientation, children: [
322
361
  sliding ? /* @__PURE__ */ jsx4("span", { ref: indicatorRef, className: `sia-tabs__indicator${type === "capsule" ? " sia-tabs__capsule-indicator" : ""}`, "aria-hidden": "true", children: type === "tech-line" ? /* @__PURE__ */ jsx4("span", { className: "sia-tabs__indicator-shape", "aria-hidden": "true" }) : null }, type) : null,
323
- items.map((item) => {
362
+ items.map((item, index) => {
324
363
  const selected = item.key === selectedKey;
325
364
  const canClose = item.closable ?? closable;
326
- const tabNode = /* @__PURE__ */ jsxs4("span", { className: `sia-tabs__tab-wrap${canClose ? " is-closable" : ""}`, children: [
327
- /* @__PURE__ */ jsxs4(
328
- "button",
329
- {
330
- id: `${baseId}-tab-${item.key}`,
331
- type: "button",
332
- className: "sia-tabs__tab",
333
- role: "tab",
334
- "aria-selected": selected,
335
- "aria-controls": hasPanels ? `${baseId}-panel-${item.key}` : void 0,
336
- tabIndex: selected ? 0 : -1,
337
- disabled: item.disabled,
338
- onClick: () => selectTab(item.key),
339
- onKeyDown: (event) => handleKeyDown(event, item.key),
340
- children: [
341
- type === "tech-line" ? /* @__PURE__ */ jsx4("span", { className: "sia-tabs__tab-shape", "aria-hidden": "true" }) : null,
342
- item.icon ? /* @__PURE__ */ jsx4("span", { className: "sia-tabs__icon", children: item.icon }) : null,
343
- /* @__PURE__ */ jsx4("span", { children: item.label })
344
- ]
345
- }
346
- ),
347
- canClose ? /* @__PURE__ */ jsx4(
348
- "button",
349
- {
350
- type: "button",
351
- className: "sia-tabs__close",
352
- "aria-label": `\u5173\u95ED${typeof item.label === "string" ? item.label : "\u6807\u7B7E\u9875"}`,
353
- disabled: item.disabled,
354
- onClick: () => onEdit?.(item.key, "remove"),
355
- children: /* @__PURE__ */ jsx4(Icon, { name: "close", size: 12 })
356
- }
357
- ) : null
358
- ] }, item.key);
359
365
  const contextMenuItems = typeof tabContextMenu === "function" ? tabContextMenu(item) : tabContextMenu;
366
+ const entryProps = { "data-tab-entry": "", "data-tab-hidden": index >= visibleCount, "aria-hidden": index >= visibleCount || void 0 };
367
+ const tabNode = /* @__PURE__ */ createElement("span", { ...!contextMenuItems?.length ? entryProps : {}, className: `sia-tabs__tab-wrap${canClose ? " is-closable" : ""}`, key: item.key }, /* @__PURE__ */ jsxs4(
368
+ "button",
369
+ {
370
+ id: `${baseId}-tab-${item.key}`,
371
+ type: "button",
372
+ className: "sia-tabs__tab",
373
+ role: "tab",
374
+ "aria-selected": selected,
375
+ "aria-controls": hasPanels ? `${baseId}-panel-${item.key}` : void 0,
376
+ tabIndex: selected ? 0 : -1,
377
+ disabled: item.disabled,
378
+ onClick: () => selectTab(item.key),
379
+ onKeyDown: (event) => handleKeyDown(event, item.key),
380
+ children: [
381
+ type === "tech-line" ? /* @__PURE__ */ jsx4("span", { className: "sia-tabs__tab-shape", "aria-hidden": "true" }) : null,
382
+ item.icon ? /* @__PURE__ */ jsx4("span", { className: "sia-tabs__icon", children: item.icon }) : null,
383
+ /* @__PURE__ */ jsx4("span", { children: item.label })
384
+ ]
385
+ }
386
+ ), canClose ? /* @__PURE__ */ jsx4(
387
+ "button",
388
+ {
389
+ type: "button",
390
+ className: "sia-tabs__close",
391
+ "aria-label": `\u5173\u95ED${typeof item.label === "string" ? item.label : "\u6807\u7B7E\u9875"}`,
392
+ disabled: item.disabled,
393
+ onClick: () => onEdit?.(item.key, "remove"),
394
+ children: /* @__PURE__ */ jsx4(Icon, { name: "close", size: 12 })
395
+ }
396
+ ) : null);
360
397
  if (!contextMenuItems?.length) return tabNode;
361
- return /* @__PURE__ */ jsx4(
398
+ return /* @__PURE__ */ createElement(
362
399
  Dropdown,
363
400
  {
401
+ ...entryProps,
402
+ key: item.key,
364
403
  className: "sia-tabs__tab-context-trigger",
365
404
  popupClassName: "sia-tabs__tab-context-menu",
366
405
  trigger: ["contextMenu"],
@@ -369,12 +408,26 @@ function Tabs({
369
408
  items: contextMenuItems,
370
409
  selectable: false,
371
410
  onClick: (info) => onTabContextMenuClick?.({ ...info, tabKey: item.key, tab: item })
372
- },
373
- children: tabNode
411
+ }
374
412
  },
375
- item.key
413
+ tabNode
376
414
  );
377
- })
415
+ }),
416
+ resolvedOrientation === "horizontal" ? /* @__PURE__ */ jsx4(
417
+ Dropdown,
418
+ {
419
+ className: "sia-tabs__more",
420
+ "data-tab-hidden": !hiddenItems.length,
421
+ trigger: ["hover", "click"],
422
+ placement: "bottomRight",
423
+ showTriggerIcon: false,
424
+ autoFocus: true,
425
+ open: moreOpen && hiddenItems.length > 0,
426
+ onOpenChange: setMoreOpen,
427
+ menu: { items: hiddenItems.map((item) => ({ key: item.key, label: item.label, icon: item.icon, disabled: item.disabled })), selectedKeys: [selectedKey], onClick: (info) => selectTab(info.key) },
428
+ children: /* @__PURE__ */ jsx4(Button, { ref: moreRef, variant: "text", icon: /* @__PURE__ */ jsx4(Icon, { name: "more-horizontal" }), "aria-label": "\u66F4\u591A\u9009\u9879\u5361", onKeyDown: (event) => handleKeyDown(event, selectedKey), className: hiddenItems.some((item) => item.key === selectedKey) ? "is-active" : void 0 })
429
+ }
430
+ ) : null
378
431
  ] }),
379
432
  tabBarExtraContent ? /* @__PURE__ */ jsx4("div", { className: "sia-tabs__extra", children: tabBarExtraContent }) : null
380
433
  ] }),
@@ -721,7 +721,7 @@ interface TabsProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
721
721
  * @defaultValue false
722
722
  */
723
723
  centered?: boolean;
724
- /** 标签栏左侧标题,科技大屏型会附带装饰线;折线科技型横向布局按标题内容及装饰留白确定宽度,窄屏时标题独占一行。 */
724
+ /** 标签栏左侧标题,支持文本或 React 节点;科技样式附带装饰线。 */
725
725
  tabBarTitle?: ReactNode;
726
726
  /** 标签栏与内容面板之间的间距,单位 px,支持 0;未指定时上下布局为 16px,左右布局为 20px。仅影响当前 Tabs,不传递给嵌套实例。 */
727
727
  contentGap?: number;
@@ -744,7 +744,7 @@ interface TabsProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
744
744
  /** 点击选项卡右键菜单时调用。 */
745
745
  onTabContextMenuClick?: (info: TabsContextMenuClickInfo) => void;
746
746
  }
747
- /** 选项卡组件。 */
747
+ /** 选项卡组件;横向空间不足时,剩余标签收起到末尾的更多菜单,支持悬停或点击展开。 */
748
748
  declare function Tabs({ items, activeKey, defaultActiveKey, onChange, onAfterChange, deferContent, type, size, tabPosition, orientation, centered, tabBarTitle, tabBarExtraContent, contentGap, destroyInactiveTabPane, closable, onEdit, tabContextMenu, onTabContextMenuClick, className, ...props }: TabsProps): react.JSX.Element;
749
749
 
750
750
  /** 打开下拉菜单的触发方式。 */
@@ -721,7 +721,7 @@ interface TabsProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
721
721
  * @defaultValue false
722
722
  */
723
723
  centered?: boolean;
724
- /** 标签栏左侧标题,科技大屏型会附带装饰线;折线科技型横向布局按标题内容及装饰留白确定宽度,窄屏时标题独占一行。 */
724
+ /** 标签栏左侧标题,支持文本或 React 节点;科技样式附带装饰线。 */
725
725
  tabBarTitle?: ReactNode;
726
726
  /** 标签栏与内容面板之间的间距,单位 px,支持 0;未指定时上下布局为 16px,左右布局为 20px。仅影响当前 Tabs,不传递给嵌套实例。 */
727
727
  contentGap?: number;
@@ -744,7 +744,7 @@ interface TabsProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
744
744
  /** 点击选项卡右键菜单时调用。 */
745
745
  onTabContextMenuClick?: (info: TabsContextMenuClickInfo) => void;
746
746
  }
747
- /** 选项卡组件。 */
747
+ /** 选项卡组件;横向空间不足时,剩余标签收起到末尾的更多菜单,支持悬停或点击展开。 */
748
748
  declare function Tabs({ items, activeKey, defaultActiveKey, onChange, onAfterChange, deferContent, type, size, tabPosition, orientation, centered, tabBarTitle, tabBarExtraContent, contentGap, destroyInactiveTabPane, closable, onEdit, tabContextMenu, onTabContextMenuClick, className, ...props }: TabsProps): react.JSX.Element;
749
749
 
750
750
  /** 打开下拉菜单的触发方式。 */