@hanamura/react-containers
Version:
Flexible container components for React
96 lines • 4.11 kB
JavaScript
'use client';
import { useState, useEffect } from 'react';
/**
* Normalizes spacing values to CSS-compatible string format
*
* @param value - A spacing value that can be a single value or an array of values
* @returns A CSS-compatible string with proper units
*
* Examples:
* - normalizeSpacingValue(16) => "16px"
* - normalizeSpacingValue("1rem") => "1rem"
* - normalizeSpacingValue([16]) => "16px"
* - normalizeSpacingValue([8, 16]) => "8px 16px"
* - normalizeSpacingValue(["1rem"]) => "1rem"
* - normalizeSpacingValue(["1rem", 8]) => "1rem 8px"
*/
export function normalizeSpacingValue(value) {
if (typeof value === 'undefined')
return undefined;
if (typeof value === 'number')
return `${value}px`;
if (typeof value === 'string')
return value;
return value.map(normalizeSpacingValue).join(' ');
}
/**
* Hook for creating responsive container components that adapt to breakpoints
*
* This hook handles media query matching and option merging to enable
* responsive behavior in container components.
*
* @param options - Base options applied to all breakpoints
* @param queries - Array of media query definitions with associated keys
* @param adaptiveOptions - Context-specific options that override defaults at different breakpoints
* @returns Object containing active context key and merged options
*
* @typeParam K - Type for breakpoint keys (e.g. 'mobile', 'tablet', 'desktop')
* @typeParam O - Type for component-specific options
*/
export function useAdaptiveContainer(options, queries, adaptiveOptions) {
// Track which breakpoint context is currently active
const [activeContext, setActiveContext] = useState(null);
// Store options overrides for the active breakpoint
const [adaptiveOverrides, setAdaptiveOverrides] = useState(null);
// Combine base options with adaptive overrides using proper types
const mergedOptions = {
...(options ?? {}),
...(adaptiveOverrides ?? {}),
};
useEffect(() => {
// Skip effect if no queries are provided
if (!queries || queries.length === 0)
return;
// Create media query listeners for each breakpoint
const mediaQueryLists = queries.map(([key, { query }]) => ({
key,
mediaQueryList: window.matchMedia(query),
}));
// Perform initial check to set the active context
updateActiveContext();
// Set up listeners for media query changes
mediaQueryLists.forEach(({ mediaQueryList }) => {
mediaQueryList.addEventListener('change', updateActiveContext);
});
/**
* Updates the active context based on which media query matches
* Media query priority follows the order in the queries array:
* - The LAST matching query has the highest priority
* - Queries should be ordered from most general to most specific
*/
function updateActiveContext() {
// Find the last (highest priority) matching media query
const match = mediaQueryLists.findLast(({ mediaQueryList }) => mediaQueryList.matches);
if (match) {
// Update the active context and its associated options
const newContext = match.key;
setActiveContext(newContext);
setAdaptiveOverrides(adaptiveOptions?.[newContext] ?? null);
}
else {
// No media queries match, reset to default options only
setActiveContext(null);
setAdaptiveOverrides(null);
}
}
// Clean up event listeners on unmount or when dependencies change
return () => {
mediaQueryLists.forEach(({ mediaQueryList }) => {
mediaQueryList.removeEventListener('change', updateActiveContext);
});
};
}, [queries, adaptiveOptions]);
// Return both the active context key and the merged options
return { activeContext, options: mergedOptions };
}
//# sourceMappingURL=useAdaptiveContainer.js.map