import React from "react"; /** * Extracts the appropriate ref type for a given element type. * * This utility type ensures that refs are properly typed based on the element * being rendered. For example, a button element receives HTMLButtonElement ref. * Excludes legacy string refs (deprecated since React 16.3). * * @typeParam C - The HTML element type (e.g., 'button', 'div', 'a') * @example * ```typescript * type ButtonRef = PolymorphicRef<'button'>; // React.Ref * type DivRef = PolymorphicRef<'div'>; // React.Ref * ``` */ type PolymorphicRef = React.Ref< React.ElementRef >; /** * Defines the 'as' prop that determines which HTML element to render. * * This is the core prop that enables polymorphic behavior, allowing components * to render as any valid React element type while maintaining type safety. * * @typeParam C - The HTML element type to render * @example * ```typescript * Click me * Link * ``` */ type AsProp = { as?: C; }; /** * Identifies props that should be omitted to prevent type conflicts. * * This type ensures that our custom props don't conflict with native element * props by calculating which keys need to be omitted from the native props. * * @typeParam C - The HTML element type * @typeParam P - The custom props to merge */ type PropsToOmit = keyof (AsProp & P); /** * Merges custom props with native element props while preventing conflicts. * * This creates a union of our custom props and the native props for the chosen * element, omitting any conflicting keys to ensure type safety. * * @typeParam C - The HTML element type * @typeParam Props - The custom props to add * @example * ```typescript * // For a button, this merges custom props with HTMLButtonElement props * type ButtonProps = PolymorphicComponentProp<'button', { variant?: string }>; * ``` */ type PolymorphicComponentProp< C extends React.ElementType, Props extends object = Record, > = React.PropsWithChildren> & Omit, PropsToOmit>; /** * Extends PolymorphicComponentProp to include properly-typed ref support. * * This is the final type in the polymorphic type chain, adding ref forwarding * with the correct ref type for the chosen element. The ref is properly typed * to match the element being rendered, enabling focus management and direct * DOM access for accessibility features like programmatic focus control. * * Supports both PolymorphicRef and ForwardedRef for compatibility with * React.forwardRef components. * * @typeParam C - The HTML element type * @typeParam Props - The custom props to add * * @example * ```typescript * // Using ref for focus management (important for accessibility) * const buttonRef = useRef(null); * useEffect(() => { * // Programmatically focus for keyboard navigation * buttonRef.current?.focus(); * }, []); * * return Accessible Button; * ``` */ type PolymorphicComponentPropWithRef< C extends React.ElementType, Props extends object = Record, > = PolymorphicComponentProp & { ref?: PolymorphicRef | React.ForwardedRef>; }; /** * Props for the UI component, extending polymorphic props with style and class support. * * The UI component automatically forwards all ARIA attributes and native HTML props * to the rendered element, ensuring full accessibility support. This includes: * - `aria-label`, `aria-labelledby` - Accessible names for screen readers * - `aria-describedby` - Additional descriptive text references * - `aria-expanded`, `aria-controls` - Interactive widget states * - `role` - Semantic role override when needed * - All other ARIA attributes and HTML props * * @typeParam C - The HTML element type to render * @property {boolean} [renderStyles] - Reserved for future use. Currently has no effect. * Styles are always rendered regardless of this prop value. * @property {React.CSSProperties} [styles] - Inline styles to apply (overrides defaultStyles) * @property {React.CSSProperties} [defaultStyles] - Base styles that can be overridden by styles prop * @property {string} [classes] - CSS class names to apply to the element (custom prop) * @property {string} [className] - CSS class names to apply to the element (React standard prop) * @property {string} [id] - HTML id attribute * @property {React.ReactNode} [children] - Child elements to render * * @example * ```typescript * // All ARIA attributes are automatically forwarded * * * * ``` */ type UIProps = PolymorphicComponentPropWithRef< C, { /** @deprecated Reserved for future use. Currently has no effect. Styles are always rendered. */ renderStyles?: boolean; styles?: React.CSSProperties; defaultStyles?: React.CSSProperties; classes?: string; className?: string; id?: string; children?: React.ReactNode; } >; /** * UI Component function signature. * * Defines the polymorphic component that can render as any HTML element while * maintaining full TypeScript type safety for element-specific props. * * @typeParam C - The HTML element type to render (defaults to 'div') * @param {UIProps} props - Component props including 'as', styles, and native element props * @returns {React.ReactElement} A React element of the specified type * @example * ```typescript * Button * Link * Default div * ``` */ type UIComponent = (( props: UIProps ) => React.ReactElement | null) & { displayName?: string }; /** * UI - A polymorphic React component that can render as any HTML element. * * The UI component is a foundational primitive used throughout fpkit to create * flexible, type-safe components. It implements the polymorphic component pattern, * allowing a single component to render as different HTML elements while maintaining * full TypeScript type safety for element-specific props. * * ## Accessibility Considerations * * The UI component forwards all ARIA attributes and native HTML props, placing * accessibility responsibility on the consumer. When creating interactive elements, * you MUST ensure WCAG 2.1 AA compliance: * * - **Accessible Names**: All interactive elements need an accessible name via * `aria-label`, `aria-labelledby`, or visible text content * - **Semantic HTML**: Prefer semantic elements (`button`, `a`, `nav`) over * generic containers (`div`, `span`) with ARIA roles when possible * - **Focus Indicators**: Ensure focus indicators meet WCAG 2.4.7 (3:1 contrast) * - **Keyboard Support**: Interactive elements must be keyboard accessible * * @typeParam C - The HTML element type to render (e.g., 'button', 'div', 'a') * * @param {C} [as='div'] - The HTML element type to render. Defaults to 'div'. * @param {React.CSSProperties} [styles] - Inline styles to apply. Overrides defaultStyles. * @param {string} [classes] - CSS class names to apply (custom prop). Takes precedence over className. * @param {string} [className] - CSS class names to apply (React standard). Used if classes is not provided. * @param {React.CSSProperties} [defaultStyles] - Base styles that can be overridden by styles prop. * @param {React.ReactNode} [children] - Child elements to render inside the component. * @param {PolymorphicRef} [ref] - Forwarded ref with proper typing for the element type. * @param {boolean} [renderStyles] - Reserved for future use. Currently has no effect. * * @returns {React.ReactElement} A React element of the specified type with merged props. * * @example * // Basic usage - renders as div * Hello World * * @example * // Polymorphic rendering - renders as button with type-safe props * * Click me * * * @example * // ✅ GOOD: Accessible button with aria-label for icon-only button * * * * * @example * // ✅ GOOD: Accessible link with descriptive text * * View all products * * * @example * // ✅ GOOD: Interactive element with proper role and keyboard support * e.key === 'Enter' && handleToggle()} * > * Menu * * * @example * // ❌ BAD: Button without accessible name (screen readers can't identify it) * * * * * @example * // ❌ BAD: Non-semantic div with click handler (not keyboard accessible) * * Click me * * * @example * // ✅ GOOD: Custom focus indicator with WCAG 2.4.7 compliant contrast * * Accessible Button * * * @example * // Style merging - defaultStyles provide base, styles override * * Red text with padding * * * @example * // Ref forwarding for focus management * const buttonRef = useRef(null); * useEffect(() => { * // Programmatically focus for keyboard navigation * buttonRef.current?.focus(); * }, []); * Auto-focused Button * * @example * // Building accessible higher-level components with TypeScript * interface AccessibleButtonProps extends React.ComponentPropsWithoutRef<'button'> { * variant?: 'primary' | 'secondary'; * // Require either aria-label or children for accessibility * 'aria-label'?: string; * children?: React.ReactNode; * } * * const AccessibleButton = React.forwardRef( * ({ variant = 'primary', ...props }, ref) => { * // Runtime check: ensure accessible name is provided * if (!props['aria-label'] && !props.children) { * console.warn('AccessibleButton requires either aria-label or children'); * } * * return ( * * ); * } * ); */ // `as unknown as UIComponent` is required: React.forwardRef returns ForwardRefExoticComponent which is // structurally incompatible with the polymorphic UIComponent call signature at the type level. // This double-cast is the standard pattern for polymorphic forwardRef components (used by Radix UI and similar libraries). // eslint-disable-next-line react/display-name -- displayName is set explicitly two lines below; ESLint can't see post-definition assignment const UI: UIComponent = React.forwardRef( ( { as, styles, style, classes, className, children, defaultStyles, ...props }: UIProps, ref?: PolymorphicRef ) => { const Component = as ?? "div"; const styleObj: React.CSSProperties = { ...defaultStyles, ...styles, ...style }; // Support both 'classes' (custom) and 'className' (React standard) // 'classes' takes precedence if both are provided const classNameValue = classes ?? className; return ( {children} ); } ) as unknown as UIComponent; export default UI; UI.displayName = "UI";