优化布局和交互
This commit is contained in:
123
src/components/MobileMainToolButtons.tsx
Normal file
123
src/components/MobileMainToolButtons.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user