chore(web-platform): release V0.6.1 工程质量优化
This commit is contained in:
@@ -1,3 +1,84 @@
|
||||
import type {KeyboardEvent,ReactNode} from 'react';
|
||||
export interface TabItem<T extends string>{value:T;label:string;icon?:ReactNode;content:ReactNode;disabled?:boolean;}
|
||||
export function Tabs<T extends string>({items,value,onValueChange,label,className='',keepMounted=true}:{items:TabItem<T>[];value:T;onValueChange:(value:T)=>void;label:string;className?:string;keepMounted?:boolean}){const active=items.find(item=>item.value===value)??items.find(item=>!item.disabled)??items[0];const navigate=(event:KeyboardEvent<HTMLButtonElement>)=>{if(!['ArrowLeft','ArrowRight','Home','End'].includes(event.key))return;const enabled=items.filter(item=>!item.disabled);if(!enabled.length)return;const current=enabled.findIndex(item=>item.value===active.value);const next=event.key==='Home'?0:event.key==='End'?enabled.length-1:event.key==='ArrowRight'?(current+1)%enabled.length:(current-1+enabled.length)%enabled.length;event.preventDefault();const item=enabled[next];onValueChange(item.value);requestAnimationFrame(()=>document.getElementById(`${label}-tab-${item.value}`)?.focus());};return <div className={`flex min-h-0 flex-1 flex-col ${className}`}><div role="tablist" aria-label={label} className="flex h-9 shrink-0 items-end gap-1 border-b border-border bg-surface px-2">{items.map(item=><button key={item.value} type="button" role="tab" id={`${label}-tab-${item.value}`} tabIndex={item.value===active.value?0:-1} aria-selected={item.value===active.value} aria-controls={`${label}-${item.value}`} disabled={item.disabled} onClick={()=>onValueChange(item.value)} onKeyDown={navigate} className={`relative flex h-8 items-center gap-1.5 px-2 text-xs font-medium focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-accent/40 ${item.value===active.value?'text-accent after:absolute after:inset-x-1 after:bottom-0 after:h-0.5 after:rounded-full after:bg-accent':'text-text-tertiary hover:text-text-primary'}`}>{item.icon}{item.label}</button>)}</div>{(keepMounted?items:[active]).map(item=><div key={item.value} id={`${label}-${item.value}`} role="tabpanel" aria-labelledby={`${label}-tab-${item.value}`} hidden={item.value!==active.value} className="panel-scroll min-h-0 flex-1 overflow-auto">{item.content}</div>)}</div>;}
|
||||
import type { KeyboardEvent, ReactNode } from 'react';
|
||||
export interface TabItem<T extends string> {
|
||||
value: T;
|
||||
label: string;
|
||||
icon?: ReactNode;
|
||||
content: ReactNode;
|
||||
disabled?: boolean;
|
||||
}
|
||||
export function Tabs<T extends string>({
|
||||
items,
|
||||
value,
|
||||
onValueChange,
|
||||
label,
|
||||
className = '',
|
||||
keepMounted = true,
|
||||
}: {
|
||||
items: TabItem<T>[];
|
||||
value: T;
|
||||
onValueChange: (value: T) => void;
|
||||
label: string;
|
||||
className?: string;
|
||||
keepMounted?: boolean;
|
||||
}) {
|
||||
const active =
|
||||
items.find((item) => item.value === value) ?? items.find((item) => !item.disabled) ?? items[0];
|
||||
const navigate = (event: KeyboardEvent<HTMLButtonElement>) => {
|
||||
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
|
||||
const enabled = items.filter((item) => !item.disabled);
|
||||
if (!enabled.length) return;
|
||||
const current = enabled.findIndex((item) => item.value === active.value);
|
||||
const next =
|
||||
event.key === 'Home'
|
||||
? 0
|
||||
: event.key === 'End'
|
||||
? enabled.length - 1
|
||||
: event.key === 'ArrowRight'
|
||||
? (current + 1) % enabled.length
|
||||
: (current - 1 + enabled.length) % enabled.length;
|
||||
event.preventDefault();
|
||||
const item = enabled[next];
|
||||
onValueChange(item.value);
|
||||
requestAnimationFrame(() => document.getElementById(`${label}-tab-${item.value}`)?.focus());
|
||||
};
|
||||
return (
|
||||
<div className={`flex min-h-0 flex-1 flex-col ${className}`}>
|
||||
<div
|
||||
role="tablist"
|
||||
aria-label={label}
|
||||
className="flex h-9 shrink-0 items-end gap-1 border-b border-border bg-surface px-2"
|
||||
>
|
||||
{items.map((item) => (
|
||||
<button
|
||||
key={item.value}
|
||||
type="button"
|
||||
role="tab"
|
||||
id={`${label}-tab-${item.value}`}
|
||||
tabIndex={item.value === active.value ? 0 : -1}
|
||||
aria-selected={item.value === active.value}
|
||||
aria-controls={`${label}-${item.value}`}
|
||||
disabled={item.disabled}
|
||||
onClick={() => onValueChange(item.value)}
|
||||
onKeyDown={navigate}
|
||||
className={`relative flex h-8 items-center gap-1.5 px-2 text-xs font-medium focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-accent/40 ${item.value === active.value ? 'text-accent after:absolute after:inset-x-1 after:bottom-0 after:h-0.5 after:rounded-full after:bg-accent' : 'text-text-tertiary hover:text-text-primary'}`}
|
||||
>
|
||||
{item.icon}
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{(keepMounted ? items : [active]).map((item) => (
|
||||
<div
|
||||
key={item.value}
|
||||
id={`${label}-${item.value}`}
|
||||
role="tabpanel"
|
||||
aria-labelledby={`${label}-tab-${item.value}`}
|
||||
hidden={item.value !== active.value}
|
||||
className="panel-scroll min-h-0 flex-1 overflow-auto"
|
||||
>
|
||||
{item.content}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user