Essential utility functions and React hooks for building accessible React Aria UI components
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Intelligent prop merging, event chaining, and DOM attribute filtering for React components. These utilities handle the complex logic of combining props from multiple sources while preserving type safety.
Intelligently merges multiple props objects with special handling for event handlers, CSS classes, and IDs.
/**
* Intelligently merges multiple props objects
* - Chains event handlers (functions starting with 'on[A-Z]')
* - Combines CSS classes using clsx
* - Merges IDs using mergeIds
* - Later props override earlier ones for other properties
* @param args - Multiple props objects to merge
* @returns Merged props object with proper TypeScript typing
*/
function mergeProps<T extends Props[]>(...args: T): UnionToIntersection<TupleTypes<T>>;Usage Examples:
import { mergeProps } from "@react-aria/utils";
function Button({ className, onClick, ...props }) {
const defaultProps = {
className: "btn btn-default",
onClick: (e) => console.log("Button clicked"),
type: "button" as const
};
const userProps = {
className,
onClick,
...props
};
// Event handlers are chained, classes combined, other props override
const finalProps = mergeProps(defaultProps, userProps);
return <button {...finalProps} />;
}
// Usage
<Button
className="btn-primary" // Results in: "btn btn-default btn-primary"
onClick={(e) => console.log("User click")} // Both handlers will run
disabled={true} // Overrides type: "button"
/>Chains multiple callback functions to execute in sequence with the same arguments.
/**
* Chains multiple callback functions to execute in sequence
* @param callbacks - Variable number of callback functions
* @returns Function that calls all callbacks with same arguments
*/
function chain(...callbacks: any[]): (...args: any[]) => void;Usage Examples:
import { chain } from "@react-aria/utils";
function MyComponent() {
const handleClick1 = (e) => console.log("First handler");
const handleClick2 = (e) => console.log("Second handler");
const handleClick3 = (e) => console.log("Third handler");
// Chain multiple handlers
const chainedHandler = chain(handleClick1, handleClick2, handleClick3);
return <button onClick={chainedHandler}>Click me</button>;
}
// All three handlers will execute in order when button is clickedFilters props to include only valid DOM attributes, with configurable options for different use cases.
/**
* Filters props to include only valid DOM attributes
* @param props - Component props to filter
* @param opts - Options object controlling which props to include
* @returns Filtered props object safe for DOM elements
*/
function filterDOMProps(
props: DOMProps,
opts?: FilterDOMPropsOptions
): DOMAttributes & AriaLabelingProps & GlobalDOMAttributes;
interface FilterDOMPropsOptions {
/** Include ARIA labeling props (aria-label, aria-labelledby, etc.) */
labelable?: boolean;
/** Include link-specific DOM props (href, target, etc.) */
isLink?: boolean;
/** Include global DOM attributes (id, className, style, etc.) */
global?: boolean;
/** Include DOM event handlers (onClick, onFocus, etc.) */
events?: boolean;
/** Additional prop names to include */
propNames?: Set<string>;
}Usage Examples:
import { filterDOMProps } from "@react-aria/utils";
function MyInput({ label, onValueChange, ...props }) {
// Filter out non-DOM props but keep labeling props
const domProps = filterDOMProps(props, {
labelable: true,
events: true
});
return (
<div>
{label && <label>{label}</label>}
<input {...domProps} />
</div>
);
}
// Usage - onValueChange will be filtered out, but aria-label will be kept
<MyInput
aria-label="Name field"
onValueChange={(value) => console.log(value)} // Filtered out
className="input-field" // Kept if global: true
onClick={(e) => console.log("Clicked")} // Kept because events: true
customProp="value" // Filtered out
/>Complex scenarios with multiple prop sources and type preservation:
import { mergeProps, filterDOMProps } from "@react-aria/utils";
function AdvancedButton({ variant = "primary", size = "medium", ...props }) {
// Base props with defaults
const baseProps = {
className: `btn btn-${variant} btn-${size}`,
type: "button" as const
};
// Accessibility props
const a11yProps = {
role: "button",
tabIndex: 0
};
// User props (filtered for DOM safety)
const userProps = filterDOMProps(props, {
labelable: true,
events: true,
global: true
});
// Merge all props in order of precedence
const finalProps = mergeProps(baseProps, a11yProps, userProps);
return <button {...finalProps} />;
}Understanding how mergeProps chains event handlers:
import { mergeProps } from "@react-aria/utils";
function EventExample() {
const props1 = {
onClick: (e) => {
console.log("First handler");
e.preventDefault(); // This will still execute
}
};
const props2 = {
onClick: (e) => {
console.log("Second handler");
// This runs after props1.onClick
}
};
const props3 = {
onClick: (e) => {
console.log("Third handler");
// This runs after props2.onClick
}
};
const mergedProps = mergeProps(props1, props2, props3);
return <button {...mergedProps}>Click me</button>;
// All three handlers execute in order: First, Second, Third
}interface DOMProps {
id?: string;
}
interface DOMAttributes extends React.DOMAttributes<HTMLElement> {
// Standard DOM event handlers and attributes
}
interface AriaLabelingProps {
"aria-label"?: string;
"aria-labelledby"?: string;
"aria-describedby"?: string;
"aria-details"?: string;
}
interface GlobalDOMAttributes {
className?: string;
style?: React.CSSProperties;
hidden?: boolean;
lang?: string;
dir?: "ltr" | "rtl";
// ... other global HTML attributes
}
type Props = Record<string, any>;
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
type TupleTypes<T> = { [P in keyof T]: T[P] };Install with Tessl CLI
npx tessl i tessl/npm-react-aria--utils