Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added `spacing`, `margin`, `padding`, `typography`, `color`, `font`, `background` and `visuallyHidden` utility
functions to generate lumx class names (exported in `classNames` namespace in `@lumx/core/js/utils` or as individual
exports in `@lumx/core/js/utils/classNames`)
- Added `block` and `element` utility functions to generate BEM class names
- `@lumx/react`:
- Added `useClassnames` utility hook to `@lumx/react/utils` providing BEM (block, element & modifier) classname generation

## [3.21.1][] - 2025-12-16

Expand Down
1 change: 1 addition & 0 deletions packages/lumx-core/src/js/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type { HasCloseMode } from './HasCloseMode';
export type { HasTheme } from './HasTheme';
export type { GenericProps } from './GenericProps';
export type { HeadingElement } from './HeadingElement';
export type { KebabCase } from './KebabCase';
export type { Point } from './Point';
export type { Predicate } from './Predicate';
export type { RectSize } from './RectSize';
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

48 changes: 48 additions & 0 deletions packages/lumx-core/src/js/utils/classNames/bem/block.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { block } from './block';

describe(block, () => {
const button = block('my-button');

it('should generate block class without modifier', () => {
expect(button()).toBe('my-button');
});

it('should generate block class with string modifier', () => {
expect(button('primary')).toBe('my-button my-button--primary');
});

it('should generate block class with object modifier', () => {
expect(button({ active: true })).toBe('my-button my-button--active');
});

it('should generate block class with multiple object modifiers', () => {
const result = button({ active: true, disabled: false, large: true });
expect(result).toContain('my-button');
expect(result).toContain('my-button--active');
expect(result).toContain('my-button--large');
expect(result).not.toContain('my-button--disabled');
});

it('should generate block class with additional classes', () => {
expect(button(undefined, 'custom-class')).toBe('my-button custom-class');
});

it('should generate block class with modifier and additional classes', () => {
const result = button('primary', 'custom-class');
expect(result).toContain('my-button');
expect(result).toContain('my-button--primary');
expect(result).toContain('custom-class');
});

it('should order classes correctly by default (modifier before additional)', () => {
expect(button('primary', 'custom-class')).toBe('my-button my-button--primary custom-class');
});

it('should handle array of additional classes', () => {
const result = button('primary', ['class1', 'class2']);
expect(result).toContain('my-button');
expect(result).toContain('my-button--primary');
expect(result).toContain('class1');
expect(result).toContain('class2');
});
});
21 changes: 21 additions & 0 deletions packages/lumx-core/src/js/utils/classNames/bem/block.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { ClassValue } from 'classnames/types';

import { generateBEMClass, type Modifier } from './generateBEMClass';

/**
* Creates a BEM block class generator function for the given base class.
* Returns a function that generates BEM class names with optional modifiers.
*
* @param baseClass - The base BEM block class name (e.g., 'button', 'card')
* @returns A function that accepts:
* - modifier - Optional BEM modifier (string, object, or array)
* - additionalClasses - Optional additional classes to include
*
* @example
* const button = block('my-button');
* button(); // 'my-button'
* button('primary'); // 'my-button my-button--primary'
* button({ active: true }); // 'my-button my-button--active'
*/
export const block = (baseClass: string) => (modifier?: Modifier, additionalClasses?: ClassValue | ClassValue[]) =>
generateBEMClass(baseClass, modifier, additionalClasses);
54 changes: 54 additions & 0 deletions packages/lumx-core/src/js/utils/classNames/bem/element.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { element } from './element';

describe(element, () => {
const button = element('my-button');

it('should generate element class without modifier', () => {
expect(button('icon')).toBe('my-button__icon');
});

it('should generate element class with string modifier', () => {
expect(button('icon', 'large')).toBe('my-button__icon my-button__icon--large');
});

it('should generate element class with object modifier', () => {
expect(button('icon', { active: true })).toBe('my-button__icon my-button__icon--active');
});

it('should generate element class with multiple object modifiers', () => {
const result = button('icon', { active: true, disabled: false, visible: true });
expect(result).toContain('my-button__icon');
expect(result).toContain('my-button__icon--active');
expect(result).toContain('my-button__icon--visible');
expect(result).not.toContain('my-button__icon--disabled');
});

it('should generate element class with additional classes', () => {
expect(button('icon', undefined, 'custom-class')).toBe('my-button__icon custom-class');
});

it('should generate element class with modifier and additional classes', () => {
const result = button('icon', 'large', 'custom-class');
expect(result).toContain('my-button__icon');
expect(result).toContain('my-button__icon--large');
expect(result).toContain('custom-class');
});

it('should order classes correctly by default (modifier before additional)', () => {
expect(button('icon', 'large', 'custom-class')).toBe('my-button__icon my-button__icon--large custom-class');
});

it('should handle array of additional classes', () => {
const result = button('icon', 'large', ['class1', 'class2']);
expect(result).toContain('my-button__icon');
expect(result).toContain('my-button__icon--large');
expect(result).toContain('class1');
expect(result).toContain('class2');
});

it('should handle different element names', () => {
expect(button('title')).toBe('my-button__title');
expect(button('body')).toBe('my-button__body');
expect(button('footer')).toBe('my-button__footer');
});
});
23 changes: 23 additions & 0 deletions packages/lumx-core/src/js/utils/classNames/bem/element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { ClassValue } from 'classnames/types';

import { generateBEMClass, type Modifier } from './generateBEMClass';

/**
* Creates a BEM element class generator function for the given base class.
* Returns a function that generates BEM element class names with optional modifiers.
*
* @param baseClass - The base BEM block class name (e.g., 'button', 'card')
* @returns A function that accepts:
* - elem - The BEM element name (e.g., 'icon', 'title')
* - modifier - Optional BEM modifier (string, object, or array)
* - additionalClasses - Optional additional classes to include
*
* @example
* const button = element('my-button');
* button('icon'); // 'my-button__icon'
* button('icon', 'large'); // 'my-button__icon my-button__icon--large'
* button('icon', { active: true }); // 'my-button__icon my-button__icon--active'
*/
export const element =
(baseClass: string) => (elem: string, modifier?: Modifier, additionalClasses?: ClassValue | ClassValue[]) =>
generateBEMClass(`${baseClass}__${elem}`, modifier, additionalClasses);
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { generateBEMClass } from './generateBEMClass';

describe(generateBEMClass, () => {
describe('without modifiers or additional classes', () => {
it('should return base class only', () => {
expect(generateBEMClass('button')).toBe('button');
});
});

describe('with string modifier', () => {
it('should generate BEM class with string modifier', () => {
expect(generateBEMClass('button', 'primary')).toBe('button button--primary');
});
});

describe('with object modifier', () => {
it('should generate BEM class with single object modifier', () => {
expect(generateBEMClass('button', { active: true })).toBe('button button--active');
});

it('should generate BEM class with multiple object modifiers', () => {
const result = generateBEMClass('button', { active: true, disabled: false, large: true });
expect(result).toContain('button');
expect(result).toContain('button--active');
expect(result).toContain('button--large');
expect(result).not.toContain('button--disabled');
});

it('should handle falsy values in object modifier', () => {
const result = generateBEMClass('button', { active: false, disabled: undefined });
expect(result).toBe('button');
});
});

describe('with array modifier', () => {
it('should pass through array as additional classes', () => {
const result = generateBEMClass('button', ['custom-class', 'another-class']);
expect(result).toContain('button');
expect(result).toContain('custom-class');
expect(result).toContain('another-class');
});
});

describe('with additional classes', () => {
it('should include single additional class', () => {
expect(generateBEMClass('button', undefined, 'custom-class')).toBe('button custom-class');
});

it('should include array of additional classes', () => {
const result = generateBEMClass('button', undefined, ['class1', 'class2']);
expect(result).toContain('button');
expect(result).toContain('class1');
expect(result).toContain('class2');
});
});

describe('with modifier and additional classes', () => {
it('should include both modifier and additional class', () => {
const result = generateBEMClass('button', 'primary', 'custom-class');
expect(result).toContain('button');
expect(result).toContain('button--primary');
expect(result).toContain('custom-class');
});

it('should order modifier before additional classes by default', () => {
expect(generateBEMClass('button', 'primary', 'custom-class')).toBe('button button--primary custom-class');
});
});

describe('with element classes', () => {
it('should work with element classes', () => {
expect(generateBEMClass('button__icon', 'large')).toBe('button__icon button__icon--large');
});

it('should handle element with object modifier', () => {
const result = generateBEMClass('button__icon', { active: true, disabled: false });
expect(result).toContain('button__icon');
expect(result).toContain('button__icon--active');
expect(result).not.toContain('button__icon--disabled');
});
});

describe('edge cases', () => {
it('should handle empty object modifier', () => {
expect(generateBEMClass('button', {})).toBe('button');
});

it('should handle empty array modifier', () => {
expect(generateBEMClass('button', [])).toBe('button');
});

it('should handle undefined values', () => {
expect(generateBEMClass('button', undefined, undefined)).toBe('button');
});
});
});
Loading
Loading