Files
label-designer/src/components/MobileMainToolButtons.tsx
2026-06-14 12:03:05 +08:00

124 lines
4.0 KiB
TypeScript

import React, { useLayoutEffect, useMemo, useRef, useState } from 'react';
import { MoreHorizontal } from 'lucide-react';
import { AnchoredMenu } from './AnchoredMenu';
const TOOL_BTN_SIZE_PX = 40;
const TOOL_BTN_GAP_PX = 4;
const TOOL_BTN_SLOT_PX = TOOL_BTN_SIZE_PX + TOOL_BTN_GAP_PX;
export interface MobileToolButtonItem {
id: string;
icon: React.ReactNode;
label: string;
shortcut?: string;
active?: boolean;
onClick: () => void;
}
interface MobileMainToolButtonsProps {
items: MobileToolButtonItem[];
}
function computeVisibleCount(containerWidth: number, total: number): number {
if (total <= 0 || containerWidth <= 0) return total;
if (containerWidth >= total * TOOL_BTN_SLOT_PX) return total;
const withMore = Math.floor((containerWidth - TOOL_BTN_SLOT_PX) / TOOL_BTN_SLOT_PX);
return Math.max(1, Math.min(withMore, total - 1));
}
function reorderItemsForActive(items: MobileToolButtonItem[], visibleCount: number): MobileToolButtonItem[] {
if (visibleCount >= items.length) return items;
const activeIndex = items.findIndex((item) => item.active);
if (activeIndex < 0 || activeIndex < visibleCount) return items;
const next = [...items];
const [activeItem] = next.splice(activeIndex, 1);
next.splice(visibleCount - 1, 0, activeItem);
return next;
}
export const MobileMainToolButtons: React.FC<MobileMainToolButtonsProps> = ({ items }) => {
const containerRef = useRef<HTMLDivElement | null>(null);
const moreButtonRef = useRef<HTMLButtonElement | null>(null);
const [visibleCount, setVisibleCount] = useState(items.length);
const [moreOpen, setMoreOpen] = useState(false);
useLayoutEffect(() => {
const container = containerRef.current;
if (!container) return;
const measure = () => {
setVisibleCount(computeVisibleCount(container.clientWidth, items.length));
};
measure();
const observer = new ResizeObserver(measure);
observer.observe(container);
return () => observer.disconnect();
}, [items.length]);
const orderedItems = useMemo(
() => reorderItemsForActive(items, visibleCount),
[items, visibleCount]
);
const visibleItems = orderedItems.slice(0, visibleCount);
const overflowItems = orderedItems.slice(visibleCount);
const overflowHasActive = overflowItems.some((item) => item.active);
const renderToolButton = (item: MobileToolButtonItem) => (
<button
key={item.id}
type="button"
className={`ps-tool-btn mobile-design-tool-btn ${item.active ? 'active' : ''}`}
onClick={item.onClick}
title={`${item.label}${item.shortcut ? ` (${item.shortcut})` : ''}`}
aria-label={item.label}
>
{item.icon}
</button>
);
return (
<div ref={containerRef} className="mobile-design-toolstrip-tools">
{visibleItems.map((item) => renderToolButton(item))}
{overflowItems.length > 0 && (
<>
<button
ref={moreButtonRef}
type="button"
className={`ps-tool-btn mobile-design-tool-btn`}
onClick={() => setMoreOpen((open) => !open)}
title="更多工具"
aria-label="更多工具"
aria-expanded={moreOpen}
>
<MoreHorizontal className="w-[18px] h-[18px]" />
</button>
<AnchoredMenu
open={moreOpen}
onClose={() => setMoreOpen(false)}
anchorRef={moreButtonRef}
className="mobile-toolstrip-overflow-menu"
>
{overflowItems.map((item) => (
<button
key={item.id}
type="button"
role="menuitem"
className={`mobile-toolstrip-overflow-menu-item${item.active ? ' active' : ''}`}
onClick={() => {
item.onClick();
setMoreOpen(false);
}}
>
<span className="mobile-toolstrip-overflow-menu-icon">{item.icon}</span>
<span>{item.label}</span>
</button>
))}
</AnchoredMenu>
</>
)}
</div>
);
};