// Copyright 2017-2026 @pezkuwi/react-components authors & contributors // SPDX-License-Identifier: Apache-2.0 import type { Text } from '@pezkuwi/types'; import React, { useMemo } from 'react'; import { useToggle } from '@pezkuwi/react-hooks'; import Icon from './Icon.js'; import { styled } from './styled.js'; interface Meta { docs: Text[]; } export interface Props { children?: React.ReactNode; className?: string; isOpen?: boolean; isHeader?: boolean; isLeft?: boolean; isPadded?: boolean; onClick?: (isOpen: boolean) => void; renderChildren?: (() => React.ReactNode | undefined | null) | null; summary?: React.ReactNode; summaryHead?: React.ReactNode; summaryMeta?: Meta; summarySub?: React.ReactNode; withBreaks?: boolean; withHidden?: boolean; } function splitSingle (value: string[], sep: string): string[] { return value.reduce((result: string[], value: string): string[] => { return value.split(sep).reduce((result: string[], value: string) => result.concat(value), result); }, []); } function splitParts (value: string): string[] { return ['[', ']'].reduce((result: string[], sep) => splitSingle(result, sep), [value]); } function formatMeta (meta?: Meta): [React.ReactNode, React.ReactNode] | null { if (!meta?.docs.length) { return null; } const strings = meta.docs.map((d) => d.toString().trim()); const firstEmpty = strings.findIndex((d) => !d.length); const combined = ( firstEmpty === -1 ? strings : strings.slice(0, firstEmpty) ).join(' ').replace(/# ?[^<]*<\/weight>/, ''); const parts = splitParts(combined.replace(/\\/g, '').replace(/`/g, '')); return [ parts[0].split(/[.(]/)[0], <>{parts.map((part, index) => index % 2 ? [{part}] : {part})}  ]; } function Expander ({ children, className = '', isHeader, isLeft, isOpen, isPadded, onClick, renderChildren, summary, summaryHead, summaryMeta, summarySub, withBreaks, withHidden }: Props): React.ReactElement { const [isExpanded, toggleExpanded] = useToggle(isOpen, onClick); const demandChildren = useMemo( () => isExpanded && renderChildren && renderChildren(), [isExpanded, renderChildren] ); const [headerSubMini, headerSub] = useMemo( () => formatMeta(summaryMeta) || [summarySub, summarySub], [summaryMeta, summarySub] ); const hasContent = useMemo( () => !!renderChildren || (!!children && (!Array.isArray(children) || children.length !== 0)), [children, renderChildren] ); const icon = useMemo( () => ( ), [hasContent, isExpanded] ); return (
{isLeft && icon}
{summaryHead}
{summary} {headerSub && (
{isExpanded ? headerSub : headerSubMini}
)}
{!isLeft && icon}
{hasContent && (isExpanded || withHidden) && (
{children || demandChildren}
)}
); } const StyledDiv = styled.div` max-width: 60rem; overflow: hidden; text-overflow: ellipsis; &:not(.isExpanded) { .ui--Expander-content { display: none; } } &.isExpanded { .ui--Expander-content { margin-top: 0.75rem; .body.column { justify-content: end; } } } &.isHeader { margin-left: 2rem; } &.withBreaks .ui--Expander-content { white-space: normal; } .ui--Expander-summary { margin: 0; min-width: 13.5rem; overflow: hidden; .ui--Expander-summary-header { display: inline-block; max-width: calc(100% - 2rem); overflow: hidden; text-overflow: ellipsis; vertical-align: middle; white-space: nowrap; span { white-space: normal; } .ui--Expander-summary-header-sub, .ui--Expander-summary-title { -webkit-box-orient: vertical; -webkit-line-clamp: 1; box-orient: vertical; display: -webkit-box; line-clamp: 1; overflow: hidden; text-overflow: ellipsis; white-space: normal; } .ui--Expander-summary-header-sub { font-size: var(--font-size-small); opacity: var(--opacity-light); } } .ui--Icon { vertical-align: middle; } &:not(.isLeft) > .ui--Icon { margin-left: 0.75rem; } &.isLeft > .ui--Icon { margin-right: 0.75rem; } .ui--LabelHelp { .ui--Icon { margin-left: 0; margin-right: 0.5rem; vertical-align: text-bottom; } } } &.hasContent .ui--Expander-summary { cursor: pointer; } &.isPadded .ui--Expander-summary { margin-left: 2.25rem; } `; export default React.memo(Expander);