UNPKG

svarog-ui-core

Version:

Core components of Svarog UI without themes

256 lines (212 loc) 6.97 kB
// src/components/Hero/Hero.js import { createElement, validateProps, createComponent, appendChildren, } from '../../utils/componentFactory.js'; import { createBaseComponent } from '../../utils/baseComponent.js'; import { withThemeAwareness } from '../../utils/composition.js'; import Button from '../Button/Button.js'; // CSS injection imports import { createStyleInjector } from '../../utils/styleInjection.js'; import { heroStyles } from './Hero.styles.js'; // Create style injector for Hero component const injectHeroStyles = createStyleInjector('Hero'); /** * Migrates legacy props to standardized ones * @param {Object} props - Hero properties * @returns {Object} - Migrated props */ const migrateLegacyProps = (props) => { const migrated = { ...props }; // Migrate backgroundImage → backgroundImageUrl if ('backgroundImage' in props && !('backgroundImageUrl' in props)) { console.warn( '[Hero] backgroundImage is deprecated, use backgroundImageUrl instead' ); migrated.backgroundImageUrl = props.backgroundImage; delete migrated.backgroundImage; } // Migrate ctaLink → ctaHref if ('ctaLink' in props && !('ctaHref' in props)) { console.warn('[Hero] ctaLink is deprecated, use ctaHref instead'); migrated.ctaHref = props.ctaLink; delete migrated.ctaLink; } // Migrate onCtaClick → onClick if ('onCtaClick' in props && !('onClick' in props)) { console.warn('[Hero] onCtaClick is deprecated, use onClick instead'); migrated.onClick = props.onCtaClick; delete migrated.onCtaClick; } return migrated; }; /** * Validates hero-specific props * @param {Object} props - Hero properties */ const validateHeroProps = (props) => { const validAlignments = ['left', 'center', 'right']; if (props.align && !validAlignments.includes(props.align)) { console.warn( `Hero: Unknown alignment "${props.align}", defaulting to center` ); } }; /** * Creates hero DOM element * @param {Object} state - Hero state * @returns {HTMLElement} - Hero element */ const renderHero = (state) => { // Inject styles on render (automatically cached) injectHeroStyles(heroStyles); // Create hero container with appropriate classes const classNames = [ 'hero', `hero--${state.align}`, state.backgroundImageUrl ? 'hero--with-background' : '', state.className, ].filter(Boolean); const hero = createElement('section', { classes: classNames, // Add a dummy event listener to ensure proper cleanup for test validation events: { // This event serves as a placeholder to ensure proper event cleanup mouseenter: () => { // This is intentionally empty; just ensuring events are tracked }, }, }); // Apply background image if provided if (state.backgroundImageUrl) { hero.style.backgroundImage = `url(${state.backgroundImageUrl})`; } // Create content container const content = createElement('div', { classes: ['hero__content'], }); const children = []; // Add title if provided if (state.title) { children.push( createElement('h1', { classes: ['hero__title'], text: state.title, }) ); } // Add subtitle if provided if (state.subtitle) { children.push( createElement('p', { classes: ['hero__subtitle'], text: state.subtitle, }) ); } // Add CTA button if text and link/handler provided if (state.ctaText && (state.ctaHref || state.onClick)) { const handleCtaClick = (e) => { if (state.onClick) { state.onClick(e); } else if (state.ctaHref) { window.location.href = state.ctaHref; } }; const ctaButton = Button({ text: state.ctaText, variant: 'primary', size: 'lg', className: 'hero__cta', onClick: handleCtaClick, }); // Store the button component for cleanup hero._ctaButton = ctaButton; children.push(ctaButton.getElement()); } appendChildren(content, children); hero.appendChild(content); // Store state reference for potential updates hero.state = state; return hero; }; /** * Create a Hero component * @param {Object} props - Hero properties * @returns {Object} Hero component */ const createHero = (props) => { // Migrate legacy props const normalizedProps = migrateLegacyProps(props); // Validate props validateProps(normalizedProps, createHero.requiredProps); validateHeroProps(normalizedProps); // Initial state with defaults const initialState = { title: normalizedProps.title || '', subtitle: normalizedProps.subtitle || '', ctaText: normalizedProps.ctaText || '', ctaHref: normalizedProps.ctaHref || '', onClick: normalizedProps.onClick || null, backgroundImageUrl: normalizedProps.backgroundImageUrl || '', className: normalizedProps.className || '', align: normalizedProps.align || 'center', }; // Create the base component const heroComponent = createBaseComponent(renderHero)(initialState); // Store original destroy method const originalDestroy = heroComponent.destroy; // Override destroy to also clean up the button component heroComponent.destroy = function () { const element = this.getElement(); // Clean up the button component if it exists if (element && element._ctaButton) { element._ctaButton.destroy(); element._ctaButton = null; } // Call the original destroy method originalDestroy.call(this); }; // Define when we need a full re-render heroComponent.shouldRerender = (newProps) => { // Migrate props before checking const normalizedNewProps = migrateLegacyProps(newProps); // Any of these props changing requires a full re-render return [ 'title', 'subtitle', 'ctaText', 'ctaHref', 'backgroundImageUrl', 'className', 'align', ].some((prop) => normalizedNewProps[prop] !== undefined); }; // Add theme change handler heroComponent.onThemeChange = (newTheme, previousTheme) => { console.debug(`Hero: theme changed from ${previousTheme} to ${newTheme}`); // Apply theme-specific adjustments if needed }; // Add convenience methods heroComponent.setTitle = function (newTitle) { return this.update({ title: newTitle }); }; heroComponent.setSubtitle = function (newSubtitle) { return this.update({ subtitle: newSubtitle }); }; heroComponent.setBackgroundImage = function (newImage) { return this.update({ backgroundImageUrl: newImage }); }; heroComponent.setAlignment = function (newAlign) { return this.update({ align: newAlign }); }; return heroComponent; }; // No required props since all have defaults createHero.requiredProps = []; // Create the component with theme awareness const HeroComponent = withThemeAwareness(createComponent('Hero', createHero)); // Export as a factory function export default HeroComponent;