'use client'; import clsx from 'classnames'; import { createContext, useContext, useId } from 'react'; import { useStore } from 'zustand'; import { getOrCreateDisclosureStoreByKey } from './getOrCreateDisclosureStoreByKey'; import { OpenAPISelect, useSelectState } from './OpenAPISelect'; interface Props { groups: TDisclosureGroup[]; icon?: React.ReactNode; /** State key to use with a store */ selectStateKey?: string; /** State key to synchronize disclosure groups across the page */ stateKey?: string; /** Icon to display for the select */ selectIcon?: React.ReactNode; className?: string; } type TDisclosureGroup = { key: string; label: string | React.ReactNode; tabs?: { key: string; label: string | React.ReactNode; body?: React.ReactNode; }[]; }; export type Key = string | number; type DisclosureGroupState = { expandedKeys: Set; toggleKey: (key: Key) => void; isDisabled: boolean; }; const DisclosureGroupStateContext = createContext(null); type DisclosureGroupProps = { /** When false, expanding a group collapses the others. */ allowsMultipleExpanded?: boolean; expandedKeys?: Iterable; defaultExpandedKeys?: Iterable; onExpandedChange?: (keys: Set) => void; isDisabled?: boolean; }; function useDisclosureGroupStore(stateKey = 'disclosure-group', initialKeys?: Iterable) { const store = useStore(getOrCreateDisclosureStoreByKey(stateKey, initialKeys)); return store; } /** * Display an interactive OpenAPI disclosure group. */ export function OpenAPIDisclosureGroup(props: DisclosureGroupProps & Props) { const { icon, groups, selectStateKey, stateKey, selectIcon, className, expandedKeys, defaultExpandedKeys, onExpandedChange, } = props; // When `expandedKeys` is provided, the group is controlled by the parent. Otherwise it owns // its expanded state in a store keyed by `stateKey`. const isControlled = expandedKeys !== undefined; const initialKeys = defaultExpandedKeys ? new Set(defaultExpandedKeys) : undefined; const { expandedKeys: storeExpandedKeys, setExpandedKeys } = useDisclosureGroupStore( stateKey, initialKeys ); const currentKeys = new Set(isControlled ? expandedKeys : storeExpandedKeys); const state: DisclosureGroupState = { expandedKeys: currentKeys, isDisabled: props.isDisabled ?? false, toggleKey: (key) => { const next = new Set(props.allowsMultipleExpanded ? currentKeys : []); if (currentKeys.has(key)) { next.delete(key); } else { next.add(key); } if (!isControlled) { setExpandedKeys(next); } onExpandedChange?.(next); }, }; return ( {groups.map((group) => ( ))} ); } function DisclosureItem(props: { group: TDisclosureGroup; icon?: React.ReactNode; selectStateKey?: string; selectIcon?: React.ReactNode; className?: string; }) { const { icon, group, selectStateKey, selectIcon, className } = props; const defaultId = useId(); const id = group.key || defaultId; const groupState = useContext(DisclosureGroupStateContext); const isExpanded = groupState?.expandedKeys.has(id) || false; const isDisabled = groupState?.isDisabled || !group.tabs?.length || false; const panelId = `${id}-panel`; const toggle = () => { if (!isDisabled) { groupState?.toggleKey(id); } }; const defaultTab = group.tabs?.[0]?.key || ''; const store = useSelectState(selectStateKey, defaultTab); const selectedTab = group.tabs?.find((tab) => tab.key === store.key) || group.tabs?.[0]; return (
{/* A div, not a button: the header hosts the media-type select. */}
{ if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); toggle(); } }} className="openapi-disclosure-group-trigger" >
{icon || ( )}
{group.label} {group.tabs ? (
e.stopPropagation()} > {group.tabs?.length > 1 ? ( { if (!isExpanded) { groupState?.toggleKey(id); } }} items={group.tabs} placement="bottom end" > {(tab) => tab.label} ) : group.tabs[0]?.label ? ( {group.tabs[0].label} ) : null}
) : null}
{isExpanded && selectedTab && (
{selectedTab.body}
)}
); }