Move the global package disclosure action into a responsive right-aligned toolbar group and disable it for empty queues. Replace the decorative package title with a compact neutral filter group, coordinate disclosure opacity with height transitions, and make contextual help follow combined pointer and keyboard focus state.
97 lines
2.4 KiB
TypeScript
97 lines
2.4 KiB
TypeScript
import { Children, Fragment, isValidElement, useId, useRef, type ReactElement, type ReactNode } from "react";
|
|
import { Icon } from "./Icon";
|
|
|
|
export interface ContextInfoButtonProps {
|
|
contextName: string;
|
|
content: ReactNode;
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
}
|
|
|
|
function hasRenderableContent(content: ReactNode): boolean {
|
|
if (content === null || content === undefined || typeof content === "boolean") {
|
|
return false;
|
|
}
|
|
if (typeof content === "string") {
|
|
return content.trim().length > 0;
|
|
}
|
|
if (typeof content === "number") {
|
|
return true;
|
|
}
|
|
if (Array.isArray(content)) {
|
|
return content.some(hasRenderableContent);
|
|
}
|
|
if (isValidElement<{ children?: ReactNode }>(content) && content.type === Fragment) {
|
|
return hasRenderableContent(content.props.children);
|
|
}
|
|
if (isValidElement(content)) {
|
|
return true;
|
|
}
|
|
return Children.toArray(content).some(hasRenderableContent);
|
|
}
|
|
|
|
export function ContextInfoButton({
|
|
contextName,
|
|
content,
|
|
open,
|
|
onOpenChange
|
|
}: ContextInfoButtonProps): ReactElement | null {
|
|
const regionId = useId();
|
|
const pointerInside = useRef(false);
|
|
const focusInside = useRef(false);
|
|
|
|
if (!hasRenderableContent(content)) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className="ui-context-info"
|
|
onBlur={(event) => {
|
|
if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {
|
|
focusInside.current = false;
|
|
if (!pointerInside.current) {
|
|
onOpenChange(false);
|
|
}
|
|
}
|
|
}}
|
|
onFocus={() => {
|
|
focusInside.current = true;
|
|
onOpenChange(true);
|
|
}}
|
|
onPointerEnter={() => {
|
|
pointerInside.current = true;
|
|
onOpenChange(true);
|
|
}}
|
|
onPointerLeave={() => {
|
|
pointerInside.current = false;
|
|
if (!focusInside.current) {
|
|
onOpenChange(false);
|
|
}
|
|
}}
|
|
>
|
|
<button
|
|
aria-controls={regionId}
|
|
aria-expanded={open}
|
|
aria-label="Informationen"
|
|
className="ui-context-info-trigger"
|
|
onClick={() => onOpenChange(true)}
|
|
title="Informationen"
|
|
type="button"
|
|
>
|
|
<Icon name="info" size={18} />
|
|
</button>
|
|
{open ? (
|
|
<div
|
|
aria-label={`Informationen zu ${contextName}`}
|
|
className="ui-context-info-region"
|
|
id={regionId}
|
|
role="region"
|
|
>
|
|
{content}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|