124 lines
4.0 KiB
TypeScript
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>
|
|
);
|
|
};
|