svarog-ui-core
Version:
Core components of Svarog UI without themes
324 lines (274 loc) • 9.76 kB
JavaScript
// src/components/Section/Section.js
import { createBaseComponent } from '../../utils/baseComponent.js';
import { createElement, appendChildren } from '../../utils/componentFactory.js';
import Typography from '../Typography/Typography.js';
// CSS injection imports
import { createStyleInjector } from '../../utils/styleInjection.js';
import { sectionStyles } from './Section.styles.js';
// Create style injector for Section component
const injectSectionStyles = createStyleInjector('Section');
/**
* Creates a Section component for defining page sections
* @param {Object} props - Section properties
* @param {string|HTMLElement|Array} props.children - Content for the section
* @param {string} [props.id] - Section ID for anchor links
* @param {string} [props.variant] - Section variant ("minor" for alternative styling)
* @param {HTMLElement} [props.backgroundImageElement] - Optional background image element
* @param {string} [props.backgroundColor] - Custom background color (CSS color value)
* @param {boolean} [props.noPaddingBottom=false] - Whether to remove bottom padding
* @param {string} [props.className=''] - Additional CSS class names
* @param {string} [props.title] - Optional section title
* @param {string} [props.description] - Optional section description
* @returns {Object} Section component API
*/
const createSection = (props) => {
// Migrate legacy props
const normalizedProps = migrateLegacyProps(props);
// Validate props
if (!normalizedProps.children) {
throw new Error('Section: children is required');
}
if (normalizedProps.variant && normalizedProps.variant !== 'minor') {
throw new Error('Section: variant must be "minor" or undefined');
}
if (
normalizedProps.backgroundImageElement &&
!(normalizedProps.backgroundImageElement instanceof HTMLElement)
) {
throw new Error('Section: backgroundImageElement must be an HTMLElement');
}
/**
* Renders the section element based on current state
* @param {Object} state - Current component state
* @returns {HTMLElement} Section element
*/
const renderSection = (state) => {
// Inject styles on render
injectSectionStyles(sectionStyles);
// Build class names
const classNames = ['section'];
if (state.className) {
classNames.push(state.className);
}
if (state.variant === 'minor') {
classNames.push('section--minor');
}
if (state.noPaddingBottom) {
classNames.push('section--no-padding-bottom');
}
// Create the main section element
const section = createElement('div', {
classes: classNames,
attributes: {
id: state.id || null,
},
style: state.backgroundColor
? { backgroundColor: state.backgroundColor }
: {},
});
// Add background image if provided
if (state.backgroundImageElement) {
const bgContainer = createElement('div', {
classes: ['section__background-image'],
});
bgContainer.appendChild(state.backgroundImageElement);
section.appendChild(bgContainer);
}
// Create content container
const content = createElement('div', {
classes: ['section__content'],
});
// Add title if provided
if (state.title) {
const titleEl = Typography({
children: state.title,
as: 'h2',
className: 'section__title',
}).getElement();
content.appendChild(titleEl);
}
// Add description if provided
if (state.description) {
const descriptionEl = Typography({
children: state.description,
as: 'p',
className: 'section__description',
}).getElement();
content.appendChild(descriptionEl);
}
// Add main content - handle both string and array children
if (typeof state.children === 'string') {
content.textContent = state.children;
} else if (state.children instanceof HTMLElement) {
content.appendChild(state.children);
} else if (Array.isArray(state.children)) {
appendChildren(content, state.children);
} else if (typeof state.children.getElement === 'function') {
content.appendChild(state.children.getElement());
}
// Add content to section
section.appendChild(content);
return section;
};
// Create component using baseComponent
const baseComponent = createBaseComponent(renderSection)(normalizedProps);
/**
* Determines if component needs to fully re-render based on prop changes
* @param {Object} newProps - New properties
* @returns {boolean} Whether a full re-render is required
*/
const shouldRerender = (newProps) => {
// Migrate legacy props
const normalizedNewProps = migrateLegacyProps(newProps);
// Only rebuild if critical props change
const criticalProps = [
'children',
'variant',
'backgroundImageElement',
'title',
'description',
];
return Object.keys(normalizedNewProps).some((key) =>
criticalProps.includes(key)
);
};
/**
* Perform partial update without full re-render
* @param {HTMLElement} element - Current element
* @param {Object} newProps - New properties
*/
const partialUpdate = (element, newProps) => {
// Migrate legacy props
const normalizedNewProps = migrateLegacyProps(newProps);
// Update background color
if (normalizedNewProps.backgroundColor !== undefined) {
element.style.backgroundColor = normalizedNewProps.backgroundColor || '';
}
// Update class names
if (
normalizedNewProps.className !== undefined ||
normalizedNewProps.noPaddingBottom !== undefined ||
normalizedNewProps.variant !== undefined
) {
const baseClasses = ['section'];
// Get current state with updates
const currentState = { ...baseComponent.state };
if (normalizedNewProps.className !== undefined) {
currentState.className = normalizedNewProps.className;
}
if (normalizedNewProps.noPaddingBottom !== undefined) {
currentState.noPaddingBottom = normalizedNewProps.noPaddingBottom;
}
if (normalizedNewProps.variant !== undefined) {
currentState.variant = normalizedNewProps.variant;
}
// Build new class names
if (currentState.className) {
baseClasses.push(currentState.className);
}
if (currentState.variant === 'minor') {
baseClasses.push('section--minor');
}
if (currentState.noPaddingBottom) {
baseClasses.push('section--no-padding-bottom');
}
element.className = baseClasses.join(' ');
}
// Update ID
if (normalizedNewProps.id !== undefined) {
if (normalizedNewProps.id) {
element.setAttribute('id', normalizedNewProps.id);
} else {
element.removeAttribute('id');
}
}
};
// Extended component with custom methods
const sectionComponent = {
...baseComponent,
/**
* Determines if component should fully re-render
* @param {Object} newProps - New properties
* @returns {boolean} Whether a full re-render is required
*/
shouldRerender,
/**
* Performs efficient partial updates
* @param {HTMLElement} element - Current element
* @param {Object} newProps - New properties
*/
partialUpdate,
/**
* Sets the section variant
* @param {string} variant - Section variant ("minor" or null)
* @returns {Object} Section component (for chaining)
*/
setVariant(variant) {
if (variant !== 'minor' && variant !== null && variant !== undefined) {
throw new Error('Section: variant must be "minor", null, or undefined');
}
return this.update({ variant });
},
/**
* Sets the background color
* @param {string} color - CSS color value
* @returns {Object} Section component (for chaining)
*/
setBackgroundColor(color) {
return this.update({ backgroundColor: color });
},
/**
* Toggles bottom padding
* @param {boolean} value - Whether to remove bottom padding
* @returns {Object} Section component (for chaining)
*/
setNoPaddingBottom(value) {
return this.update({ noPaddingBottom: !!value });
},
/**
* Sets the section title
* @param {string} title - Section title
* @returns {Object} Section component (for chaining)
*/
setTitle(title) {
return this.update({ title });
},
/**
* Sets the section description
* @param {string} description - Section description
* @returns {Object} Section component (for chaining)
*/
setDescription(description) {
return this.update({ description });
},
};
// Add theme change handler
sectionComponent.onThemeChange = (newTheme, previousTheme) => {
console.debug(
`Section: theme changed from ${previousTheme} to ${newTheme}`
);
// We could apply theme-specific adjustments here if needed
};
return sectionComponent;
};
/**
* Migrates legacy props to standardized props
* @param {Object} props - Component properties
* @returns {Object} Normalized properties
*/
const migrateLegacyProps = (props) => {
const migrated = { ...props };
if ('backgroundImage' in props && !('backgroundImageElement' in props)) {
console.warn(
'[Section] backgroundImage is deprecated, use backgroundImageElement instead'
);
migrated.backgroundImageElement = props.backgroundImage;
delete migrated.backgroundImage;
}
return migrated;
};
// Define required props for validation
createSection.requiredProps = ['children'];
// Export as both named export and default export
export { createSection as createSection };
export default createSection;