import { clsx } from 'clsx'; import { createContext, useContext, useMemo, useRef, useState } from 'react'; import { useResizeObserver } from '../common/hooks/useResizeObserver'; import { cssValueWithUnit } from '../utilities/cssValueWithUnit'; import { FieldLabelContextProvider, InputDescribedByProvider, InputIdContextProvider, InputInvalidProvider, } from './contexts'; type InputPaddingContextType = [ number | string | undefined, React.Dispatch>, ]; const InputPaddingStartContext = createContext([undefined, () => {}]); const InputPaddingEndContext = createContext([undefined, () => {}]); export function useInputPaddings() { const [paddingStart] = useContext(InputPaddingStartContext); const [paddingEnd] = useContext(InputPaddingEndContext); return { paddingInlineStart: paddingStart, paddingInlineEnd: paddingEnd, } satisfies React.CSSProperties; } interface InputGroupAddon { content: React.ReactNode; initialContentWidth?: number | string; interactive?: boolean; padding?: 'none' | 'sm' | 'md'; } export interface InputGroupProps { addonStart?: InputGroupAddon; addonEnd?: InputGroupAddon; disabled?: boolean; className?: string; children?: React.ReactNode; } function inputPaddingInitialState({ initialContentWidth, padding = inputAddonDefaultPadding, }: Pick< InputGroupAddon, 'initialContentWidth' | 'padding' > = {}): () => InputPaddingContextType[0] { return () => initialContentWidth != null ? `calc(${cssValueWithUnit(initialContentWidth)} + ${cssValueWithUnit( inputAddonContentWidthAddendByPadding[padding], )})` : undefined; } export function InputGroup({ addonStart, addonEnd, disabled, className, children, }: InputGroupProps) { const [paddingStart, setPaddingStart] = useState(inputPaddingInitialState(addonStart)); const [paddingEnd, setPaddingEnd] = useState(inputPaddingInitialState(addonEnd)); return ( [paddingStart, setPaddingStart], [paddingStart])} > [paddingEnd, setPaddingEnd], [paddingEnd])} >
{addonStart != null ? : null} {children} {addonEnd != null ? : null}
); } interface InputAddonProps extends Omit { placement: 'start' | 'end'; } const inputAddonContentWidthAddendByPadding = { none: 0, sm: '1rem', md: '1.5rem', } satisfies { [key in NonNullable]: InputPaddingContextType[0]; }; const inputAddonDefaultPadding = 'md' satisfies InputAddonProps['padding']; function InputAddon({ placement, content, interactive, padding = inputAddonDefaultPadding, }: InputAddonProps) { const [, setInputPadding] = useContext( placement === 'start' ? InputPaddingStartContext : InputPaddingEndContext, ); const ref = useRef(null); useResizeObserver(ref, (entry) => { // TODO: Remove fallback once most browsers support `borderBoxSize` const inlineSize = entry.borderBoxSize?.[0]?.inlineSize; if (inlineSize != null) { setInputPadding(inlineSize); } else { const targetStyle = getComputedStyle(entry.target); setInputPadding( entry.contentRect.width + Number.parseFloat(targetStyle.paddingInlineStart) + Number.parseFloat(targetStyle.paddingInlineEnd), ); } }); const isAvatarView = (node: unknown): boolean => { if (!node || typeof node !== 'object') return false; const { type } = node as { type?: { displayName?: string; name?: string } }; if (!type || (typeof type !== 'function' && typeof type !== 'object')) return false; return type.displayName === 'AvatarView' || type.name === 'AvatarView'; }; const hasAvatarView = Array.isArray(content) ? content.some(isAvatarView) : isAvatarView(content); return ( /* Prevent nested controls from being labeled redundantly */ {content} ); }