From a78bb436ba30036951a3da708d020b140187bdde Mon Sep 17 00:00:00 2001 From: Guillaume Cornut Date: Wed, 10 Dec 2025 11:24:45 +0100 Subject: [PATCH 1/5] feat(lum-core): add BEM class name utility functions --- CHANGELOG.md | 1 + packages/lumx-core/src/js/types/index.ts | 1 + .../src/js/utils/classNames/bem/block.test.ts | 48 ++++++++++ .../src/js/utils/classNames/bem/block.ts | 21 ++++ .../js/utils/classNames/bem/element.test.ts | 54 +++++++++++ .../src/js/utils/classNames/bem/element.ts | 23 +++++ .../classNames/bem/generateBEMClass.test.ts | 96 +++++++++++++++++++ .../utils/classNames/bem/generateBEMClass.ts | 63 ++++++++++++ .../src/js/utils/classNames/bem/index.ts | 2 + .../src/js/utils/classNames/index.ts | 1 + 10 files changed, 310 insertions(+) create mode 100644 packages/lumx-core/src/js/utils/classNames/bem/block.test.ts create mode 100644 packages/lumx-core/src/js/utils/classNames/bem/block.ts create mode 100644 packages/lumx-core/src/js/utils/classNames/bem/element.test.ts create mode 100644 packages/lumx-core/src/js/utils/classNames/bem/element.ts create mode 100644 packages/lumx-core/src/js/utils/classNames/bem/generateBEMClass.test.ts create mode 100644 packages/lumx-core/src/js/utils/classNames/bem/generateBEMClass.ts create mode 100644 packages/lumx-core/src/js/utils/classNames/bem/index.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e7f0368f7..b65eb52f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ 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 ## [3.21.1][] - 2025-12-16 diff --git a/packages/lumx-core/src/js/types/index.ts b/packages/lumx-core/src/js/types/index.ts index 6a4fe0fac..80abe094e 100644 --- a/packages/lumx-core/src/js/types/index.ts +++ b/packages/lumx-core/src/js/types/index.ts @@ -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'; diff --git a/packages/lumx-core/src/js/utils/classNames/bem/block.test.ts b/packages/lumx-core/src/js/utils/classNames/bem/block.test.ts new file mode 100644 index 000000000..e4f988308 --- /dev/null +++ b/packages/lumx-core/src/js/utils/classNames/bem/block.test.ts @@ -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'); + }); +}); diff --git a/packages/lumx-core/src/js/utils/classNames/bem/block.ts b/packages/lumx-core/src/js/utils/classNames/bem/block.ts new file mode 100644 index 000000000..0fa9ce4a3 --- /dev/null +++ b/packages/lumx-core/src/js/utils/classNames/bem/block.ts @@ -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); diff --git a/packages/lumx-core/src/js/utils/classNames/bem/element.test.ts b/packages/lumx-core/src/js/utils/classNames/bem/element.test.ts new file mode 100644 index 000000000..b7ba82972 --- /dev/null +++ b/packages/lumx-core/src/js/utils/classNames/bem/element.test.ts @@ -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'); + }); +}); diff --git a/packages/lumx-core/src/js/utils/classNames/bem/element.ts b/packages/lumx-core/src/js/utils/classNames/bem/element.ts new file mode 100644 index 000000000..8fb09ffa6 --- /dev/null +++ b/packages/lumx-core/src/js/utils/classNames/bem/element.ts @@ -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); diff --git a/packages/lumx-core/src/js/utils/classNames/bem/generateBEMClass.test.ts b/packages/lumx-core/src/js/utils/classNames/bem/generateBEMClass.test.ts new file mode 100644 index 000000000..2d2ca1f16 --- /dev/null +++ b/packages/lumx-core/src/js/utils/classNames/bem/generateBEMClass.test.ts @@ -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'); + }); + }); +}); diff --git a/packages/lumx-core/src/js/utils/classNames/bem/generateBEMClass.ts b/packages/lumx-core/src/js/utils/classNames/bem/generateBEMClass.ts new file mode 100644 index 000000000..2dcf75ff5 --- /dev/null +++ b/packages/lumx-core/src/js/utils/classNames/bem/generateBEMClass.ts @@ -0,0 +1,63 @@ +import classnames from 'classnames'; +import type { ClassValue } from 'classnames/types'; + +/** + * Modifier + * @example 'is-disabled' + * @example { 'is-disabled': true, 'is-selected': false } + * @example ['another-class'] // => Added as-is, not as a BEM modifier suffix + */ +export type Modifier = string | Record | ClassValue[]; + +function generateModifierClasses(baseClass: string, modifier?: Modifier) { + if (!modifier) { + return undefined; + } + + if (Array.isArray(modifier)) { + return modifier; + } + + if (typeof modifier === 'string') { + return `${baseClass}--${modifier}`; + } + + const classes = []; + for (const [key, value] of Object.entries(modifier)) { + if (value) classes.push(`${baseClass}--${key}`); + } + return classes; +} + +/** + * Generates a BEM (Block Element Modifier) class name string. + * Combines a base class with optional modifiers and additional classes. + * + * @param baseClass - The base BEM class (block or element) + * @param modifier - Optional modifier as: + * - string: creates `baseClass--modifier` + * - object: creates `baseClass--key` for each truthy value + * - array: passes through as additional classes + * @param additionalClasses - Optional additional classes to include + * @returns Combined class name string + * + * @example + * generateBEMClass('button'); // 'button' + * generateBEMClass('button', 'primary'); // 'button button--primary' + * generateBEMClass('button', { active: true, disabled: false }); // 'button button--active' + * generateBEMClass('button', 'primary', 'my-class'); // 'button button--primary my-class' + */ +export function generateBEMClass( + baseClass: string, + modifier?: Modifier, + additionalClasses?: ClassValue | ClassValue[], +) { + return classnames( + // Base class + baseClass, + // Modifier(s) + generateModifierClasses(baseClass, modifier), + // Additional classes + additionalClasses, + ); +} diff --git a/packages/lumx-core/src/js/utils/classNames/bem/index.ts b/packages/lumx-core/src/js/utils/classNames/bem/index.ts new file mode 100644 index 000000000..94c6ed5a8 --- /dev/null +++ b/packages/lumx-core/src/js/utils/classNames/bem/index.ts @@ -0,0 +1,2 @@ +export { block } from './block'; +export { element } from './element'; diff --git a/packages/lumx-core/src/js/utils/classNames/index.ts b/packages/lumx-core/src/js/utils/classNames/index.ts index c79022f40..fe3cc4e01 100644 --- a/packages/lumx-core/src/js/utils/classNames/index.ts +++ b/packages/lumx-core/src/js/utils/classNames/index.ts @@ -3,3 +3,4 @@ export * from './color'; export * from './typography'; export * from './spacing'; export * from './visually-hidden'; +export * as bem from './bem'; From d62ece7aecf8c65825f351a298ddb5f66c466c37 Mon Sep 17 00:00:00 2001 From: Guillaume Cornut Date: Wed, 10 Dec 2025 11:52:02 +0100 Subject: [PATCH 2/5] feat(lumx-react): add useClassnames utility hook --- CHANGELOG.md | 2 + .../lumx-react/src/utils/className/index.ts | 1 + .../src/utils/className/useClassnames.ts | 48 +++++++++++++++++++ packages/lumx-react/src/utils/index.ts | 1 + 4 files changed, 52 insertions(+) create mode 100644 packages/lumx-react/src/utils/className/index.ts create mode 100644 packages/lumx-react/src/utils/className/useClassnames.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index b65eb52f7..4136777d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 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 diff --git a/packages/lumx-react/src/utils/className/index.ts b/packages/lumx-react/src/utils/className/index.ts new file mode 100644 index 000000000..347b3caf4 --- /dev/null +++ b/packages/lumx-react/src/utils/className/index.ts @@ -0,0 +1 @@ +export { useClassnames } from './useClassnames'; diff --git a/packages/lumx-react/src/utils/className/useClassnames.ts b/packages/lumx-react/src/utils/className/useClassnames.ts new file mode 100644 index 000000000..ee0cc8e72 --- /dev/null +++ b/packages/lumx-react/src/utils/className/useClassnames.ts @@ -0,0 +1,48 @@ +import { useMemo } from 'react'; + +import { classNames } from '@lumx/core/js/utils'; + +/** + * Hook that provides BEM class name utilities for a component. + * + * @param className - Base component class name (kebab-case) + * @returns Object with `block` and `element` utility functions + * + * @example + * const { block, element } = useClassnames('my-component'); + */ +export function useClassnames(className: string) { + return useMemo(() => { + return { + /** + * Generates BEM block class names with optional modifiers. + * + * @param modifier - Modifier string, object, or additional class names + * @param additionalClasses - Additional CSS classes to append + * @returns Generated class name string + * + * @example + * block() // 'my-component' + * block('active') // 'my-component my-component--active' + * block({ active: true, disabled: false }) // 'my-component my-component--active' + * block(['custom-class']) // 'my-component custom-class' + */ + block: classNames.bem.block(className), + /** + * Generates BEM element class names with optional modifiers. + * + * @param elementName - Element name + * @param modifier - Modifier string, object, or additional class names + * @param additionalClasses - Additional CSS classes to append + * @returns Generated class name string + * + * @example + * element('header') // 'my-component__header' + * element('header', 'large') // 'my-component__header my-component__header--large' + * element('header', { large: true, small: false }) // 'my-component__header my-component__header--large' + * element('header', ['custom-class']) // 'my-component__header custom-class' + */ + element: classNames.bem.element(className), + }; + }, [className]); +} diff --git a/packages/lumx-react/src/utils/index.ts b/packages/lumx-react/src/utils/index.ts index 11d573ad8..502b7786d 100644 --- a/packages/lumx-react/src/utils/index.ts +++ b/packages/lumx-react/src/utils/index.ts @@ -5,3 +5,4 @@ export { ClickAwayProvider } from './ClickAwayProvider'; export { Portal, type PortalProps, type PortalInit, PortalProvider, type PortalProviderProps } from './Portal'; export { DisabledStateProvider, useDisabledStateContext } from './disabled'; +export { useClassnames } from './className'; From de17e79927da93cb830e6fc4cd0ad99e0e5fea29 Mon Sep 17 00:00:00 2001 From: Guillaume Cornut Date: Wed, 10 Dec 2025 14:49:37 +0100 Subject: [PATCH 3/5] refactor(lumx-react): migrate to new BEM class utils --- .../.storybook/story-block/StoryBlock.tsx | 14 ++-- .../components/alert-dialog/AlertDialog.tsx | 13 ++-- .../components/autocomplete/Autocomplete.tsx | 5 +- .../AutocompleteMultiple.test.tsx | 2 +- .../autocomplete/AutocompleteMultiple.tsx | 5 +- .../src/components/avatar/Avatar.tsx | 19 ++++-- .../lumx-react/src/components/badge/Badge.tsx | 12 +++- .../src/components/badge/BadgeWrapper.tsx | 7 +- .../src/components/button/Button.test.tsx | 4 +- .../src/components/button/Button.tsx | 18 ++--- .../src/components/button/ButtonGroup.tsx | 5 +- .../src/components/button/ButtonRoot.tsx | 47 +++++++------ .../src/components/checkbox/Checkbox.tsx | 37 +++++----- .../src/components/chip/Chip.test.tsx | 4 +- .../lumx-react/src/components/chip/Chip.tsx | 40 +++++------ .../src/components/chip/ChipGroup.tsx | 5 +- .../components/comment-block/CommentBlock.tsx | 46 ++++++------- .../date-picker/DatePickerControlled.tsx | 27 ++++---- .../src/components/dialog/Dialog.tsx | 67 ++++++++----------- .../src/components/divider/Divider.tsx | 11 ++- .../src/components/drag-handle/DragHandle.tsx | 11 ++- .../src/components/dropdown/Dropdown.tsx | 7 +- .../expansion-panel/ExpansionPanel.tsx | 50 ++++++-------- .../lumx-react/src/components/flag/Flag.tsx | 15 +++-- .../src/components/flex-box/FlexBox.tsx | 32 +++++---- .../generic-block/GenericBlock.test.tsx | 18 ++--- .../components/generic-block/GenericBlock.tsx | 29 ++------ .../src/components/grid-column/GridColumn.tsx | 5 +- .../lumx-react/src/components/grid/Grid.tsx | 19 ++++-- .../src/components/grid/GridItem.tsx | 13 +++- .../src/components/heading/Heading.tsx | 5 +- .../lumx-react/src/components/icon/Icon.tsx | 34 +++++----- .../src/components/image-block/ImageBlock.tsx | 40 ++++++----- .../components/image-block/ImageCaption.tsx | 53 ++++----------- .../image-lightbox/ImageLightbox.tsx | 6 +- .../image-lightbox/internal/ImageSlide.tsx | 6 +- .../internal/ImageSlideshow.tsx | 15 ++--- .../src/components/inline-list/InlineList.tsx | 21 +++--- .../components/input-helper/InputHelper.tsx | 12 +++- .../src/components/input-label/InputLabel.tsx | 14 ++-- .../src/components/lightbox/Lightbox.tsx | 25 ++++--- .../components/link-preview/LinkPreview.tsx | 29 ++++---- .../lumx-react/src/components/link/Link.tsx | 22 +++--- .../lumx-react/src/components/list/List.tsx | 15 +++-- .../src/components/list/ListDivider.tsx | 5 +- .../src/components/list/ListItem.tsx | 39 +++++------ .../src/components/list/ListSubheader.tsx | 5 +- .../src/components/message/Message.tsx | 21 +++--- .../src/components/mosaic/Mosaic.tsx | 27 +++++--- .../src/components/navigation/Navigation.tsx | 18 ++--- .../components/navigation/NavigationItem.tsx | 26 +++---- .../navigation/NavigationSection.tsx | 29 ++++---- .../components/notification/Notification.tsx | 25 +++---- .../popover-dialog/PopoverDialog.tsx | 5 +- .../src/components/popover/Popover.tsx | 22 +++--- .../src/components/post-block/PostBlock.tsx | 34 ++++++---- .../progress-tracker/ProgressTracker.tsx | 11 +-- .../progress-tracker/ProgressTrackerStep.tsx | 25 ++++--- .../ProgressTrackerStepPanel.tsx | 6 +- .../src/components/progress/Progress.tsx | 12 +++- .../components/progress/ProgressCircular.tsx | 21 ++++-- .../components/progress/ProgressLinear.tsx | 15 +++-- .../components/radio-button/RadioButton.tsx | 35 +++++----- .../components/radio-button/RadioGroup.tsx | 5 +- .../src/components/select/Select.test.tsx | 6 +- .../src/components/select/Select.tsx | 43 +++++------- .../components/select/SelectMultiple.test.tsx | 6 +- .../src/components/select/SelectMultiple.tsx | 41 +++++------- .../components/select/WithSelectContext.tsx | 35 +++++----- .../side-navigation/SideNavigation.test.tsx | 2 +- .../side-navigation/SideNavigation.tsx | 6 +- .../side-navigation/SideNavigationItem.tsx | 33 +++++---- .../components/skeleton/SkeletonCircle.tsx | 13 +++- .../components/skeleton/SkeletonRectangle.tsx | 25 ++++--- .../skeleton/SkeletonTypography.tsx | 15 +++-- .../src/components/slider/Slider.tsx | 40 +++++------ .../src/components/slideshow/Slides.tsx | 20 +++--- .../slideshow/SlideshowControls.tsx | 48 ++++++------- .../components/slideshow/SlideshowItem.tsx | 5 +- .../slideshow/SlideshowItemGroup.tsx | 5 +- .../src/components/switch/Switch.tsx | 41 ++++++------ .../lumx-react/src/components/table/Table.tsx | 12 ++-- .../src/components/table/TableBody.tsx | 5 +- .../src/components/table/TableCell.tsx | 22 +++--- .../src/components/table/TableHeader.tsx | 5 +- .../src/components/table/TableRow.tsx | 18 ++--- .../lumx-react/src/components/tabs/Tab.tsx | 12 ++-- .../src/components/tabs/TabList.tsx | 15 +++-- .../src/components/tabs/TabPanel.tsx | 11 ++- .../components/text-field/RawInputText.tsx | 15 +++-- .../text-field/RawInputTextarea.tsx | 15 +++-- .../components/text-field/TextField.test.tsx | 17 ++--- .../src/components/text-field/TextField.tsx | 61 +++++++++-------- .../lumx-react/src/components/text/Text.tsx | 25 ++++--- .../src/components/thumbnail/Thumbnail.tsx | 59 ++++++++-------- .../src/components/toolbar/Toolbar.tsx | 23 +++---- .../src/components/tooltip/Tooltip.tsx | 23 +++---- .../src/components/uploader/Uploader.tsx | 32 ++++----- .../src/components/user-block/UserBlock.tsx | 34 +++++----- .../templates/FunctionalComponent.tsx.ejs | 10 +-- 100 files changed, 1054 insertions(+), 1014 deletions(-) diff --git a/packages/lumx-react/.storybook/story-block/StoryBlock.tsx b/packages/lumx-react/.storybook/story-block/StoryBlock.tsx index a44982f2b..618b34854 100644 --- a/packages/lumx-react/.storybook/story-block/StoryBlock.tsx +++ b/packages/lumx-react/.storybook/story-block/StoryBlock.tsx @@ -1,5 +1,5 @@ import React, { ElementType } from 'react'; -import { classNames } from '@lumx/core/js/utils'; +import { useClassnames } from '@lumx/react/utils'; import isChromatic from 'chromatic/isChromatic'; import { toggleMaterialTheme } from './toggleMaterialTheme'; import { ThemeProvider } from '@lumx/react'; @@ -20,6 +20,7 @@ export const StoryBlock: React.FC = (props) => { const { Story, context } = props; const { theme, materialTheme } = context.globals; const appliedTheme = context.args.theme || theme; + const { block } = useClassnames(CLASSNAME); // Hard code today date for stable chromatic stories snapshots. context.parameters.today = isChromatic() ? new Date('May 25 2021 01:00') : new Date(); @@ -27,11 +28,16 @@ export const StoryBlock: React.FC = (props) => { if (isChromatic()) return ; React.useEffect(() => { - toggleMaterialTheme(materialTheme !== 'true') - }, [materialTheme]) + toggleMaterialTheme(materialTheme !== 'true'); + }, [materialTheme]); return ( -
+
diff --git a/packages/lumx-react/src/components/alert-dialog/AlertDialog.tsx b/packages/lumx-react/src/components/alert-dialog/AlertDialog.tsx index 0c79db6ff..c1b493552 100644 --- a/packages/lumx-react/src/components/alert-dialog/AlertDialog.tsx +++ b/packages/lumx-react/src/components/alert-dialog/AlertDialog.tsx @@ -13,11 +13,10 @@ import { ButtonProps, } from '@lumx/react'; import { mdiAlert, mdiAlertCircle, mdiCheckCircle, mdiInformation } from '@lumx/icons'; -import { handleBasicClasses } from '@lumx/core/js/utils/_internal/className'; +import { useClassnames } from '@lumx/react/utils'; import { useId } from '@lumx/react/hooks/useId'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; import type { LumxClassName } from '@lumx/react/utils/type'; -import { classNames } from '@lumx/core/js/utils'; export interface AlertDialogProps extends Omit { /** Message variant. */ @@ -97,6 +96,7 @@ export const AlertDialog = forwardRef((props, const cancelButtonRef = React.useRef(null); const confirmationButtonRef = React.useRef(null); const { color, icon } = CONFIG[kind as Kind] || {}; + const { block } = useClassnames(CLASSNAME); // Define a unique ID to target title and description for aria attributes. const generatedId = useId(); @@ -122,12 +122,11 @@ export const AlertDialog = forwardRef((props, 'aria-describedby': descriptionId, ...dialogProps, }} - className={classNames.join( + className={block( + { + [`kind-${kind}`]: Boolean(kind), + }, className, - handleBasicClasses({ - kind, - prefix: CLASSNAME, - }), )} {...forwardedProps} > diff --git a/packages/lumx-react/src/components/autocomplete/Autocomplete.tsx b/packages/lumx-react/src/components/autocomplete/Autocomplete.tsx index 3818f785e..8c882f829 100644 --- a/packages/lumx-react/src/components/autocomplete/Autocomplete.tsx +++ b/packages/lumx-react/src/components/autocomplete/Autocomplete.tsx @@ -4,11 +4,11 @@ import { Dropdown, DropdownProps, IconButtonProps, Offset, Placement, TextField, import { GenericProps, HasTheme } from '@lumx/react/utils/type'; import type { LumxClassName } from '@lumx/core/js/types'; -import { classNames } from '@lumx/core/js/utils'; import { useFocus } from '@lumx/react/hooks/useFocus'; import { mergeRefs } from '@lumx/react/utils/react/mergeRefs'; import { useTheme } from '@lumx/react/utils/theme/ThemeContext'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; +import { useClassnames } from '@lumx/react/utils'; import { useDisableStateProps } from '@lumx/react/utils/disabled/useDisableStateProps'; @@ -240,12 +240,13 @@ export const Autocomplete = forwardRef((props focusAnchorOnClose, ...forwardedProps } = otherProps; + const { block } = useClassnames(CLASSNAME); const inputAnchorRef = useRef(null); const textFieldRef = useRef(null); useFocus(inputAnchorRef.current, !isOpen && shouldFocusOnClose); return ( -
+
`, () => { const { autocompleteMultiple, textField, getDropdown } = setup({}); expect(autocompleteMultiple).toBeInTheDocument(); - expect(autocompleteMultiple.className).toMatchInlineSnapshot('"lumx-autocomplete-multiple lumx-autocomplete"'); + expect(autocompleteMultiple.className).toMatchInlineSnapshot(`"lumx-autocomplete lumx-autocomplete-multiple"`); expect(textField).toBeInTheDocument(); expect(getDropdown()).not.toBeInTheDocument(); }); diff --git a/packages/lumx-react/src/components/autocomplete/AutocompleteMultiple.tsx b/packages/lumx-react/src/components/autocomplete/AutocompleteMultiple.tsx index 0dbe32a4e..e13a942ee 100644 --- a/packages/lumx-react/src/components/autocomplete/AutocompleteMultiple.tsx +++ b/packages/lumx-react/src/components/autocomplete/AutocompleteMultiple.tsx @@ -4,9 +4,9 @@ import { mdiClose } from '@lumx/icons'; import { Autocomplete, AutocompleteProps, Chip, HorizontalAlignment, Icon, Size } from '@lumx/react'; import type { LumxClassName } from '@lumx/core/js/types'; -import { classNames } from '@lumx/core/js/utils'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; import { useTheme } from '@lumx/react/utils/theme/ThemeContext'; +import { useClassnames } from '@lumx/react/utils'; import { useDisableStateProps } from '@lumx/react/utils/disabled/useDisableStateProps'; @@ -109,13 +109,14 @@ export const AutocompleteMultiple = forwardRef((props, ref) => { ...forwardedProps } = props; + const { block, element } = useClassnames(CLASSNAME); + return (
((props, ref) => { alt={alt} theme={theme} /> - {actions &&
{actions}
} - {badge &&
{badge}
} + {actions &&
{actions}
} + {badge &&
{badge}
}
); }); diff --git a/packages/lumx-react/src/components/badge/Badge.tsx b/packages/lumx-react/src/components/badge/Badge.tsx index abf450e9f..7a063f200 100644 --- a/packages/lumx-react/src/components/badge/Badge.tsx +++ b/packages/lumx-react/src/components/badge/Badge.tsx @@ -2,9 +2,8 @@ import { ReactNode } from 'react'; import { ColorPalette } from '@lumx/react'; import { GenericProps } from '@lumx/react/utils/type'; -import { handleBasicClasses } from '@lumx/core/js/utils/_internal/className'; +import { useClassnames } from '@lumx/react/utils'; import type { LumxClassName } from '@lumx/core/js/types'; -import { classNames } from '@lumx/core/js/utils'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; /** @@ -43,11 +42,18 @@ const DEFAULT_PROPS: Partial = { */ export const Badge = forwardRef((props, ref) => { const { children, className, color = DEFAULT_PROPS.color, ...forwardedProps } = props; + const { block } = useClassnames(CLASSNAME); + return (
{children}
diff --git a/packages/lumx-react/src/components/badge/BadgeWrapper.tsx b/packages/lumx-react/src/components/badge/BadgeWrapper.tsx index b9a246ff1..e29d78b91 100644 --- a/packages/lumx-react/src/components/badge/BadgeWrapper.tsx +++ b/packages/lumx-react/src/components/badge/BadgeWrapper.tsx @@ -1,9 +1,9 @@ import { ReactElement, ReactNode } from 'react'; import type { LumxClassName } from '@lumx/core/js/types'; -import { classNames } from '@lumx/core/js/utils'; import { GenericProps } from '@lumx/react/utils/type'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; +import { useClassnames } from '@lumx/react/utils'; export interface BadgeWrapperProps extends GenericProps { /** Badge. */ @@ -24,11 +24,12 @@ const CLASSNAME: LumxClassName = 'lumx-badge-wrapper'; export const BadgeWrapper = forwardRef((props, ref) => { const { badge, children, className, ...forwardedProps } = props; + const { block, element } = useClassnames(CLASSNAME); return ( -
+
{children} - {badge &&
{badge}
} + {badge &&
{badge}
}
); }); diff --git a/packages/lumx-react/src/components/button/Button.test.tsx b/packages/lumx-react/src/components/button/Button.test.tsx index ee1068f6f..10630e1eb 100644 --- a/packages/lumx-react/src/components/button/Button.test.tsx +++ b/packages/lumx-react/src/components/button/Button.test.tsx @@ -31,7 +31,7 @@ describe(`<${Button.displayName}>`, () => { expect(button).toBe(screen.queryByRole('button', { name: label })); expect(button).toHaveAttribute('type', 'button'); expect(button.className).toMatchInlineSnapshot( - '"lumx-button lumx-button--color-primary lumx-button--emphasis-high lumx-button--size-m lumx-button--theme-light lumx-button--variant-button"', + `"lumx-button lumx-button--color-primary lumx-button--emphasis-high lumx-button--size-m lumx-button--theme-light lumx-button--variant-button lumx-button"`, ); expect(icons.length).toBe(0); }); @@ -52,7 +52,7 @@ describe(`<${Button.displayName}>`, () => { it('should render emphasis low', () => { const { button } = setup({ emphasis: Emphasis.low }); expect(button.className).toMatchInlineSnapshot( - '"lumx-button lumx-button--color-dark lumx-button--emphasis-low lumx-button--size-m lumx-button--variant-button"', + `"lumx-button lumx-button--color-dark lumx-button--emphasis-low lumx-button--size-m lumx-button--variant-button lumx-button"`, ); }); diff --git a/packages/lumx-react/src/components/button/Button.tsx b/packages/lumx-react/src/components/button/Button.tsx index 70f88262f..721259373 100644 --- a/packages/lumx-react/src/components/button/Button.tsx +++ b/packages/lumx-react/src/components/button/Button.tsx @@ -1,10 +1,7 @@ -import isEmpty from 'lodash/isEmpty'; - import { Emphasis, Icon, Size, Theme, Text, ThemeProvider } from '@lumx/react'; import { isComponent } from '@lumx/react/utils/type'; -import { getBasicClass } from '@lumx/core/js/utils/_internal/className'; +import { useClassnames } from '@lumx/react/utils'; import type { LumxClassName } from '@lumx/core/js/types'; -import { classNames } from '@lumx/core/js/utils'; import { useTheme } from '@lumx/react/utils/theme/ThemeContext'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; @@ -67,11 +64,14 @@ export const Button = forwardRef - {leftIcon && !isEmpty(leftIcon) && ( + {leftIcon && ( // Theme is handled in the button scss )} {children && (isComponent(Text)(children) ? children : {children})} - {rightIcon && !isEmpty(rightIcon) && ( + {rightIcon && ( // Theme is handled in the button scss diff --git a/packages/lumx-react/src/components/button/ButtonGroup.tsx b/packages/lumx-react/src/components/button/ButtonGroup.tsx index e33e2d6ec..edc8a6b2c 100644 --- a/packages/lumx-react/src/components/button/ButtonGroup.tsx +++ b/packages/lumx-react/src/components/button/ButtonGroup.tsx @@ -1,7 +1,7 @@ import { GenericProps } from '@lumx/react/utils/type'; import type { LumxClassName } from '@lumx/core/js/types'; -import { classNames } from '@lumx/core/js/utils'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; +import { useClassnames } from '@lumx/react/utils'; /** * Defines the props of the component @@ -37,9 +37,10 @@ const DEFAULT_PROPS: Partial = {}; */ export const ButtonGroup = forwardRef((props, ref) => { const { children, className, ...forwardedProps } = props; + const { block } = useClassnames(CLASSNAME); return ( -
+
{children}
); diff --git a/packages/lumx-react/src/components/button/ButtonRoot.tsx b/packages/lumx-react/src/components/button/ButtonRoot.tsx index 5ef240b3f..49bbebf43 100644 --- a/packages/lumx-react/src/components/button/ButtonRoot.tsx +++ b/packages/lumx-react/src/components/button/ButtonRoot.tsx @@ -2,7 +2,7 @@ import { AriaAttributes, ButtonHTMLAttributes, DetailedHTMLProps, RefObject } fr import { ColorPalette, Emphasis, Size, Theme } from '@lumx/react'; import { GenericProps, HasTheme } from '@lumx/react/utils/type'; -import { handleBasicClasses } from '@lumx/core/js/utils/_internal/className'; +import { useClassnames } from '@lumx/react/utils'; import { classNames } from '@lumx/core/js/utils'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; import { HasAriaDisabled } from '@lumx/react/utils/type/HasAriaDisabled'; @@ -57,6 +57,8 @@ const COMPONENT_NAME = 'ButtonRoot'; export const BUTTON_WRAPPER_CLASSNAME = `lumx-button-wrapper`; export const BUTTON_CLASSNAME = `lumx-button`; +const wrapperBlock = classNames.bem.block(BUTTON_WRAPPER_CLASSNAME); + /** * Render a button wrapper with the ButtonRoot inside. * @@ -69,14 +71,11 @@ const renderButtonWrapper: React.FC = (props) => { const adaptedColor = emphasis === Emphasis.low && (color === ColorPalette.light ? ColorPalette.dark : ColorPalette.light); - const wrapperClassName = classNames.join( - handleBasicClasses({ - color: adaptedColor, - prefix: BUTTON_WRAPPER_CLASSNAME, - variant, - fullWidth, - }), - ); + const wrapperClassName = wrapperBlock({ + [`color-${adaptedColor}`]: Boolean(adaptedColor), + [`variant-${variant}`]: Boolean(variant), + 'is-full-width': fullWidth, + }); const buttonProps = { ...props, hasBackground: false }; return ( @@ -114,6 +113,7 @@ export const ButtonRoot = forwardRef((props, ref) = inputProps = {}, ...forwardedProps } = otherProps; + const { block, element } = useClassnames(CLASSNAME); const localInputRef = React.useRef(null); const generatedInputId = useId(); const inputId = id || generatedInputId; @@ -106,24 +106,23 @@ export const Checkbox = forwardRef((props, ref) =
-
+
((props, ref) = {...inputProps} /> -
-
+
+
-
+
-
+
{label && ( - + {label} )} {helper && ( - + {helper} )} diff --git a/packages/lumx-react/src/components/chip/Chip.test.tsx b/packages/lumx-react/src/components/chip/Chip.test.tsx index c58377756..33a53553d 100644 --- a/packages/lumx-react/src/components/chip/Chip.test.tsx +++ b/packages/lumx-react/src/components/chip/Chip.test.tsx @@ -30,7 +30,7 @@ describe('', () => { expect(chip).toBeInTheDocument(); expect(chip).toHaveTextContent('Chip text'); expect(chip.className).toMatchInlineSnapshot( - '"lumx-chip lumx-chip--color-dark lumx-chip--size-m lumx-chip--is-unselected"', + `"lumx-chip lumx-chip--is-unselected lumx-chip--color-dark lumx-chip--size-m"`, ); }); @@ -44,7 +44,7 @@ describe('', () => { const { chip } = setup({ children: 'Chip text', onClick }); expect(chip).toHaveAttribute('role', 'button'); expect(chip.className).toMatchInlineSnapshot( - '"lumx-chip lumx-chip--is-clickable lumx-chip--color-dark lumx-chip--size-m lumx-chip--is-unselected"', + `"lumx-chip lumx-chip--is-clickable lumx-chip--is-unselected lumx-chip--color-dark lumx-chip--size-m"`, ); }); diff --git a/packages/lumx-react/src/components/chip/Chip.tsx b/packages/lumx-react/src/components/chip/Chip.tsx index 5475f26fc..8ca764f04 100644 --- a/packages/lumx-react/src/components/chip/Chip.tsx +++ b/packages/lumx-react/src/components/chip/Chip.tsx @@ -6,9 +6,9 @@ import { ColorPalette, Size, Theme } from '@lumx/react'; import { useStopPropagation } from '@lumx/react/hooks/useStopPropagation'; import { GenericProps, HasTheme } from '@lumx/react/utils/type'; -import { handleBasicClasses } from '@lumx/core/js/utils/_internal/className'; +import { onEnterPressed } from '@lumx/core/js/utils'; +import { useClassnames } from '@lumx/react/utils'; import type { LumxClassName } from '@lumx/core/js/types'; -import { classNames, onEnterPressed } from '@lumx/core/js/utils'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; import { useTheme } from '@lumx/react/utils/theme/ThemeContext'; import { useDisableStateProps } from '@lumx/react/utils/disabled/useDisableStateProps'; @@ -92,6 +92,7 @@ export const Chip = forwardRef((props, ref) => { onKeyDown, ...forwardedProps } = otherProps; + const { block, element } = useClassnames(CLASSNAME); const hasAfterClick = isFunction(onAfterClick); const hasBeforeClick = isFunction(onBeforeClick); const hasOnClick = isFunction(props.onClick); @@ -118,20 +119,19 @@ export const Chip = forwardRef((props, ref) => { {...forwardedProps} href={!disabledStateProps.disabled ? href : undefined} ref={ref} - className={classNames.join( + className={block( + { + 'is-clickable': Boolean(isClickable), + 'is-disabled': Boolean(isAnyDisabled), + 'is-highlighted': Boolean(isHighlighted), + 'is-selected': Boolean(isSelected), + 'is-unselected': Boolean(!isSelected), + 'has-after': Boolean(after), + 'has-before': Boolean(before), + [`color-${chipColor}`]: Boolean(chipColor), + [`size-${size}`]: Boolean(size), + }, className, - handleBasicClasses({ - clickable: isClickable, - color: chipColor, - isDisabled: isAnyDisabled, - hasAfter: Boolean(after), - hasBefore: Boolean(before), - highlighted: Boolean(isHighlighted), - prefix: CLASSNAME, - selected: Boolean(isSelected), - size, - unselected: Boolean(!isSelected), - }), )} aria-disabled={(isClickable && isAnyDisabled) || undefined} onClick={hasOnClick ? onClick : undefined} @@ -140,21 +140,17 @@ export const Chip = forwardRef((props, ref) => { {before && ( // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
{before}
)} -
{children}
+
{children}
{after && ( // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
{after} diff --git a/packages/lumx-react/src/components/chip/ChipGroup.tsx b/packages/lumx-react/src/components/chip/ChipGroup.tsx index caf98e877..7a8ad32b3 100644 --- a/packages/lumx-react/src/components/chip/ChipGroup.tsx +++ b/packages/lumx-react/src/components/chip/ChipGroup.tsx @@ -3,8 +3,8 @@ import { ReactNode } from 'react'; import { HorizontalAlignment } from '@lumx/core/js/constants'; import { GenericProps } from '@lumx/react/utils/type'; import type { LumxClassName } from '@lumx/core/js/types'; -import { classNames } from '@lumx/core/js/utils'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; +import { useClassnames } from '@lumx/react/utils'; import { useChipGroupNavigation } from '@lumx/react/hooks/useChipGroupNavigation'; @@ -45,9 +45,10 @@ const CLASSNAME: LumxClassName = 'lumx-chip-group'; */ const InternalChipGroup = forwardRef((props, ref) => { const { align, children, className, ...forwardedProps } = props; + const { block } = useClassnames(CLASSNAME); return ( -
+
{children}
); diff --git a/packages/lumx-react/src/components/comment-block/CommentBlock.tsx b/packages/lumx-react/src/components/comment-block/CommentBlock.tsx index 7d29be0c7..41c8b263e 100644 --- a/packages/lumx-react/src/components/comment-block/CommentBlock.tsx +++ b/packages/lumx-react/src/components/comment-block/CommentBlock.tsx @@ -2,9 +2,8 @@ import { Children, ReactNode } from 'react'; import { Avatar, Size, Theme, Tooltip } from '@lumx/react'; import { GenericProps, HasTheme, ValueOf } from '@lumx/react/utils/type'; -import { handleBasicClasses } from '@lumx/core/js/utils/_internal/className'; +import { useClassnames } from '@lumx/react/utils'; import type { LumxClassName } from '@lumx/core/js/types'; -import { classNames } from '@lumx/core/js/utils'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; import { useTheme } from '@lumx/react/utils/theme/ThemeContext'; @@ -112,34 +111,35 @@ export const CommentBlock = forwardRef((props } = props; const hasChildren = Children.count(children) > 0; + const { block, element } = useClassnames(CLASSNAME); + return (
-
-
+
+
-
-
-
+
+
+
{name && ( // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions ((props {name} )} - {headerActions && {headerActions}} + {headerActions && {headerActions}}
-
{text}
+
{text}
{date && (fullDate ? ( - {date} + {date} ) : ( - {date} + {date} ))}
- {hasActions &&
{actions}
} + {hasActions &&
{actions}
}
- {hasChildren && isOpen &&
{children}
} + {hasChildren && isOpen &&
{children}
}
); }); diff --git a/packages/lumx-react/src/components/date-picker/DatePickerControlled.tsx b/packages/lumx-react/src/components/date-picker/DatePickerControlled.tsx index e198d765d..c4c7e79a1 100644 --- a/packages/lumx-react/src/components/date-picker/DatePickerControlled.tsx +++ b/packages/lumx-react/src/components/date-picker/DatePickerControlled.tsx @@ -23,6 +23,7 @@ import { classNames, onEnterPressed } from '@lumx/core/js/utils'; import { addMonthResetDay } from '@lumx/react/utils/date/addMonthResetDay'; import { formatDayNumber } from '@lumx/react/utils/date/formatDayNumber'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; +import { useClassnames } from '@lumx/react/utils'; import { CLASSNAME } from './constants'; @@ -72,6 +73,7 @@ export const DatePickerControlled = forwardRef +
{onMonthChange && ( ) : ( @@ -204,19 +206,18 @@ export const DatePickerControlled = forwardRef } /> -
-
+
+
{weekDays.map(({ letter, number, long }) => ( -
- +
+ {letter.toLocaleUpperCase()} {long}
))}
- -
+
{weeks.flatMap((week, weekIndex) => { return weekDays.map((weekDay, dayIndex) => { const { date, isOutOfRange } = week[weekDay.number] || {}; @@ -225,12 +226,12 @@ export const DatePickerControlled = forwardRef +
{date && ( diff --git a/packages/lumx-react/src/components/popover-dialog/PopoverDialog.tsx b/packages/lumx-react/src/components/popover-dialog/PopoverDialog.tsx index 3f0048cbd..1154210dc 100644 --- a/packages/lumx-react/src/components/popover-dialog/PopoverDialog.tsx +++ b/packages/lumx-react/src/components/popover-dialog/PopoverDialog.tsx @@ -1,7 +1,7 @@ import { HasAriaLabelOrLabelledBy } from '@lumx/react/utils/type'; import type { LumxClassName } from '@lumx/core/js/types'; -import { classNames } from '@lumx/core/js/utils'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; +import { useClassnames } from '@lumx/react/utils'; import { HeadingLevelProvider } from '@lumx/react/components/heading'; import { Popover, PopoverProps } from '../popover/Popover'; @@ -44,12 +44,13 @@ export const PopoverDialog = forwardRef((pro className, ...forwardedProps } = props; + const { block } = useClassnames(CLASSNAME); return ( = { // Inner component (must be wrapped before export) const _InnerPopover = forwardRef((props, ref) => { + const { block, element } = useClassnames(CLASSNAME); const { anchorRef, as: Component = 'div', @@ -154,15 +153,14 @@ const _InnerPopover = forwardRef((props, ref) => { ((props, ref) => { {unmountSentinel} {hasArrow && ( -
+
diff --git a/packages/lumx-react/src/components/post-block/PostBlock.tsx b/packages/lumx-react/src/components/post-block/PostBlock.tsx index c7c3d0a61..13ec5b54b 100644 --- a/packages/lumx-react/src/components/post-block/PostBlock.tsx +++ b/packages/lumx-react/src/components/post-block/PostBlock.tsx @@ -4,11 +4,10 @@ import isObject from 'lodash/isObject'; import { Orientation, Theme, Thumbnail, ThumbnailProps, ThumbnailVariant } from '@lumx/react'; import { GenericProps, HasTheme } from '@lumx/react/utils/type'; -import { handleBasicClasses } from '@lumx/core/js/utils/_internal/className'; import type { LumxClassName } from '@lumx/core/js/types'; -import { classNames } from '@lumx/core/js/utils'; import { useTheme } from '@lumx/react/utils/theme/ThemeContext'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; +import { useClassnames } from '@lumx/react/utils'; /** * Defines the props of the component. @@ -77,41 +76,48 @@ export const PostBlock = forwardRef((props, ref) title, ...forwardedProps } = props; + const { block, element } = useClassnames(CLASSNAME); return (
{thumbnailProps && ( -
+
)} -
- {author &&
{author}
} +
+ {author &&
{author}
} {title && ( - )} - {meta && {meta}} + {meta && {meta}} {isObject(text) && text.__html ? ( // eslint-disable-next-line react/no-danger -

+

) : ( -

{text}

+

{text}

)} - {attachments &&
{attachments}
} + {attachments &&
{attachments}
} {(tags || actions) && ( -
- {tags &&
{tags}
} - {actions &&
{actions}
} +
+ {tags &&
{tags}
} + {actions &&
{actions}
}
)}
diff --git a/packages/lumx-react/src/components/progress-tracker/ProgressTracker.tsx b/packages/lumx-react/src/components/progress-tracker/ProgressTracker.tsx index f8ae2bca6..c191dea90 100644 --- a/packages/lumx-react/src/components/progress-tracker/ProgressTracker.tsx +++ b/packages/lumx-react/src/components/progress-tracker/ProgressTracker.tsx @@ -2,9 +2,9 @@ import React, { ReactNode } from 'react'; import { GenericProps } from '@lumx/react/utils/type'; import type { LumxClassName } from '@lumx/core/js/types'; -import { classNames } from '@lumx/core/js/utils'; import { mergeRefs } from '@lumx/react/utils/react/mergeRefs'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; +import { useClassnames } from '@lumx/react/utils'; import { useRovingTabIndex } from '../../hooks/useRovingTabIndex'; import { useTabProviderContextState } from '../tabs/state'; @@ -45,6 +45,7 @@ const DEFAULT_PROPS: Partial = {}; */ export const ProgressTracker = forwardRef((props, ref) => { const { 'aria-label': ariaLabel, children, className, ...forwardedProps } = props; + const { block, element } = useClassnames(CLASSNAME); const stepListRef = React.useRef(null); useRovingTabIndex({ parentRef: stepListRef, @@ -60,18 +61,18 @@ export const ProgressTracker = forwardRef( numberOfSteps > 0 ? ((100 / (numberOfSteps - 1)) * (state?.activeTabIndex || 0)) / 100 : 0; return ( -
-
+
+
{children}
{ if (isAnyDisabled) { @@ -121,15 +121,14 @@ export const ProgressTrackerStep = forwardRef - + - + {label} {helper && ( - + {helper} )} diff --git a/packages/lumx-react/src/components/progress-tracker/ProgressTrackerStepPanel.tsx b/packages/lumx-react/src/components/progress-tracker/ProgressTrackerStepPanel.tsx index f3f21f21c..3f8b66743 100644 --- a/packages/lumx-react/src/components/progress-tracker/ProgressTrackerStepPanel.tsx +++ b/packages/lumx-react/src/components/progress-tracker/ProgressTrackerStepPanel.tsx @@ -1,8 +1,7 @@ import { useTabProviderContext } from '@lumx/react/components/tabs/state'; import { GenericProps } from '@lumx/react/utils/type'; -import { handleBasicClasses } from '@lumx/core/js/utils/_internal/className'; -import { classNames } from '@lumx/core/js/utils'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; +import { useClassnames } from '@lumx/react/utils'; /** * Defines the props of the component. @@ -45,13 +44,14 @@ export const ProgressTrackerStepPanel = forwardRef = { export const Progress = forwardRef((props, ref) => { const defaultTheme = useTheme() || Theme.light; const { className, theme = defaultTheme, variant = DEFAULT_PROPS.variant, ...forwardedProps } = props; + const { block } = useClassnames(CLASSNAME); return (
{variant === ProgressVariant.circular && } {variant === ProgressVariant.linear && } diff --git a/packages/lumx-react/src/components/progress/ProgressCircular.tsx b/packages/lumx-react/src/components/progress/ProgressCircular.tsx index e8a2dd389..fa344e7de 100644 --- a/packages/lumx-react/src/components/progress/ProgressCircular.tsx +++ b/packages/lumx-react/src/components/progress/ProgressCircular.tsx @@ -1,8 +1,7 @@ import { Theme, Size } from '@lumx/react'; import { GenericProps, HasTheme } from '@lumx/react/utils/type'; -import { handleBasicClasses } from '@lumx/core/js/utils/_internal/className'; +import { useClassnames } from '@lumx/react/utils'; import type { LumxClassName } from '@lumx/core/js/types'; -import { classNames } from '@lumx/core/js/utils'; import { useTheme } from '@lumx/react/utils/theme/ThemeContext'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; @@ -61,18 +60,26 @@ export const ProgressCircular = forwardRef - - + + - - + + ); diff --git a/packages/lumx-react/src/components/progress/ProgressLinear.tsx b/packages/lumx-react/src/components/progress/ProgressLinear.tsx index 5485809d1..fb8055beb 100644 --- a/packages/lumx-react/src/components/progress/ProgressLinear.tsx +++ b/packages/lumx-react/src/components/progress/ProgressLinear.tsx @@ -1,10 +1,9 @@ import { Theme } from '@lumx/react'; import { GenericProps, HasTheme } from '@lumx/react/utils/type'; -import { handleBasicClasses } from '@lumx/core/js/utils/_internal/className'; import type { LumxClassName } from '@lumx/core/js/types'; -import { classNames } from '@lumx/core/js/utils'; import { useTheme } from '@lumx/react/utils/theme/ThemeContext'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; +import { useClassnames } from '@lumx/react/utils'; export interface ProgressLinearProps extends GenericProps, HasTheme {} @@ -33,15 +32,21 @@ const DEFAULT_PROPS: Partial = {}; export const ProgressLinear = forwardRef((props, ref) => { const defaultTheme = useTheme() || Theme.light; const { className, theme = defaultTheme, ...forwardedProps } = props; + const { block, element } = useClassnames(CLASSNAME); return (
-
-
+
+
); }); diff --git a/packages/lumx-react/src/components/radio-button/RadioButton.tsx b/packages/lumx-react/src/components/radio-button/RadioButton.tsx index b26c59448..25799175c 100644 --- a/packages/lumx-react/src/components/radio-button/RadioButton.tsx +++ b/packages/lumx-react/src/components/radio-button/RadioButton.tsx @@ -2,14 +2,13 @@ import { ReactNode, SyntheticEvent, InputHTMLAttributes } from 'react'; import { InputHelper, InputLabel, Theme } from '@lumx/react'; import { GenericProps, HasTheme } from '@lumx/react/utils/type'; -import { handleBasicClasses } from '@lumx/core/js/utils/_internal/className'; import type { LumxClassName } from '@lumx/core/js/types'; -import { classNames } from '@lumx/core/js/utils'; import { useId } from '@lumx/react/hooks/useId'; import { useTheme } from '@lumx/react/utils/theme/ThemeContext'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; import { useDisableStateProps } from '@lumx/react/utils/disabled/useDisableStateProps'; import { HasAriaDisabled } from '@lumx/react/utils/type/HasAriaDisabled'; +import { useClassnames } from '@lumx/react/utils'; /** * Defines the props of the component. @@ -77,6 +76,7 @@ export const RadioButton = forwardRef((props, inputProps, ...forwardedProps } = otherProps; + const { block, element } = useClassnames(CLASSNAME); const generatedInputId = useId(); const inputId = id || generatedInputId; @@ -90,21 +90,20 @@ export const RadioButton = forwardRef((props,
-
+
((props, {...inputProps} /> -
-
-
+
+
+
-
+
{label && ( - + {label} )} {helper && ( - + {helper} )} diff --git a/packages/lumx-react/src/components/radio-button/RadioGroup.tsx b/packages/lumx-react/src/components/radio-button/RadioGroup.tsx index b313ec09c..a89014368 100644 --- a/packages/lumx-react/src/components/radio-button/RadioGroup.tsx +++ b/packages/lumx-react/src/components/radio-button/RadioGroup.tsx @@ -2,8 +2,8 @@ import { ReactNode } from 'react'; import { GenericProps } from '@lumx/react/utils/type'; import type { LumxClassName } from '@lumx/core/js/types'; -import { classNames } from '@lumx/core/js/utils'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; +import { useClassnames } from '@lumx/react/utils'; /** * Defines the props of the component. @@ -32,9 +32,10 @@ const CLASSNAME: LumxClassName = 'lumx-radio-group'; */ export const RadioGroup = forwardRef((props, ref) => { const { children, className, ...forwardedProps } = props; + const { block } = useClassnames(CLASSNAME); return ( -
+
{children}
); diff --git a/packages/lumx-react/src/components/select/Select.test.tsx b/packages/lumx-react/src/components/select/Select.test.tsx index 91ec1383c..dc2de511f 100644 --- a/packages/lumx-react/src/components/select/Select.test.tsx +++ b/packages/lumx-react/src/components/select/Select.test.tsx @@ -41,7 +41,7 @@ describe(`<${Select.displayName}>`, () => { const { select, getDropdown } = setup(); expect(getDropdown()).not.toBeInTheDocument(); expect(select.className).toMatchInlineSnapshot( - '"lumx-select lumx-select--has-unique lumx-select lumx-select--is-empty lumx-select--theme-light"', + `"lumx-select lumx-select--is-empty lumx-select--theme-light lumx-select lumx-select--has-unique"`, ); }); @@ -119,7 +119,7 @@ describe(`<${Select.displayName}>`, () => { expect(inputWrapper).not.toBeInTheDocument(); expect(chip).toBeInTheDocument(); expect(select.className).toMatchInlineSnapshot( - '"lumx-select lumx-select--has-unique lumx-select lumx-select--is-empty lumx-select--theme-light"', + `"lumx-select lumx-select--is-empty lumx-select--theme-light lumx-select lumx-select--has-unique"`, ); }); @@ -127,7 +127,7 @@ describe(`<${Select.displayName}>`, () => { const { select, chip, props } = setup({ variant: SelectVariant.chip, value: 'val1' }); expect(chip).toHaveTextContent(props.value); expect(select.className).toMatchInlineSnapshot( - '"lumx-select lumx-select--has-unique lumx-select lumx-select--has-value lumx-select--theme-light"', + `"lumx-select lumx-select--has-value lumx-select--theme-light lumx-select lumx-select--has-unique"`, ); }); diff --git a/packages/lumx-react/src/components/select/Select.tsx b/packages/lumx-react/src/components/select/Select.tsx index 5dd8c5564..2d4b0a185 100644 --- a/packages/lumx-react/src/components/select/Select.tsx +++ b/packages/lumx-react/src/components/select/Select.tsx @@ -8,11 +8,10 @@ import { IconButton } from '@lumx/react/components/button/IconButton'; import { Chip } from '@lumx/react/components/chip/Chip'; import { Icon } from '@lumx/react/components/icon/Icon'; import { InputLabel } from '@lumx/react/components/input-label/InputLabel'; -import { handleBasicClasses } from '@lumx/core/js/utils/_internal/className'; import type { LumxClassName } from '@lumx/core/js/types'; -import { classNames } from '@lumx/core/js/utils'; import { mergeRefs } from '@lumx/react/utils/react/mergeRefs'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; +import { useClassnames } from '@lumx/react/utils'; import { useTheme } from '@lumx/react/utils/theme/ThemeContext'; import { WithSelectContext } from './WithSelectContext'; @@ -69,18 +68,15 @@ const SelectField: React.FC = (props) => { ...forwardedProps } = props; + const { element } = useClassnames(CLASSNAME); + return ( <> {variant === SelectVariant.input && ( <> {label && ( -
- +
+ {label}
@@ -90,7 +86,7 @@ const SelectField: React.FC = (props) => {
, selectElementRef)} id={id} - className={`${CLASSNAME}__wrapper`} + className={element('wrapper')} onClick={onInputClick} onKeyDown={handleKeyboardNav} tabIndex={isDisabled ? undefined : 0} @@ -99,26 +95,21 @@ const SelectField: React.FC = (props) => { > {icon && ( )} -
+
{!isEmpty && {selectedValueRender?.(value)}} {isEmpty && placeholder && {placeholder}}
{(isValid || hasError) && ( -
+
)} @@ -126,7 +117,7 @@ const SelectField: React.FC = (props) => { {hasInputClear && clearButtonProps && ( = (props) => { /> )} -
+
@@ -174,19 +165,19 @@ const SelectField: React.FC = (props) => { export const Select = forwardRef((props, ref) => { const isEmpty = lodashIsEmpty(props.value); const hasInputClear = props.onClear && props.clearButtonProps && !isEmpty; + const { block } = useClassnames(CLASSNAME); return WithSelectContext( SelectField, { ...DEFAULT_PROPS, ...props, - className: classNames.join( + className: block( + { + 'has-input-clear': hasInputClear, + 'has-unique': !props.isEmpty, + }, props.className, - handleBasicClasses({ - hasInputClear, - hasUnique: !props.isEmpty, - prefix: CLASSNAME, - }), ), hasInputClear, isEmpty, diff --git a/packages/lumx-react/src/components/select/SelectMultiple.test.tsx b/packages/lumx-react/src/components/select/SelectMultiple.test.tsx index 10548db97..68ad3296a 100644 --- a/packages/lumx-react/src/components/select/SelectMultiple.test.tsx +++ b/packages/lumx-react/src/components/select/SelectMultiple.test.tsx @@ -46,7 +46,7 @@ describe('', () => { expect(valueChips).toBeEmptyDOMElement(); expect(chip).not.toBeInTheDocument(); expect(select.className).toMatchInlineSnapshot( - '"lumx-select lumx-select--has-multiple lumx-select lumx-select--is-empty lumx-select--theme-light"', + `"lumx-select lumx-select--is-empty lumx-select--theme-light lumx-select lumx-select--has-multiple"`, ); }); @@ -117,7 +117,7 @@ describe('', () => { expect(inputWrapper).not.toBeInTheDocument(); expect(chip).toBeInTheDocument(); expect(select.className).toMatchInlineSnapshot( - '"lumx-select lumx-select--has-multiple lumx-select lumx-select--is-empty lumx-select--theme-light"', + `"lumx-select lumx-select--is-empty lumx-select--theme-light lumx-select lumx-select--has-multiple"`, ); }); @@ -125,7 +125,7 @@ describe('', () => { const { select, chip } = setup({ variant: SelectVariant.chip, value: ['val1', 'val2'] }); expect(chip).toHaveTextContent('val1 +1'); expect(select.className).toMatchInlineSnapshot( - '"lumx-select lumx-select--has-multiple lumx-select lumx-select--has-value lumx-select--theme-light"', + `"lumx-select lumx-select--has-value lumx-select--theme-light lumx-select lumx-select--has-multiple"`, ); }); diff --git a/packages/lumx-react/src/components/select/SelectMultiple.tsx b/packages/lumx-react/src/components/select/SelectMultiple.tsx index 796ebd4f7..440502176 100644 --- a/packages/lumx-react/src/components/select/SelectMultiple.tsx +++ b/packages/lumx-react/src/components/select/SelectMultiple.tsx @@ -5,13 +5,11 @@ import { Size, Theme } from '@lumx/core/js/constants'; import { Chip } from '@lumx/react/components/chip/Chip'; import { Icon } from '@lumx/react/components/icon/Icon'; import { InputLabel } from '@lumx/react/components/input-label/InputLabel'; -import { handleBasicClasses } from '@lumx/core/js/utils/_internal/className'; +import { useClassnames } from '@lumx/react/utils'; import type { LumxClassName } from '@lumx/core/js/types'; -import { classNames } from '@lumx/core/js/utils'; import { mergeRefs } from '@lumx/react/utils/react/mergeRefs'; import { useTheme } from '@lumx/react/utils/theme/ThemeContext'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; - import { WithSelectContext } from './WithSelectContext'; import { CoreSelectProps, SelectVariant } from './constants'; @@ -59,6 +57,7 @@ const DEFAULT_PROPS: Partial = { export const SelectMultipleField: React.FC = (props) => { const defaultTheme = useTheme(); + const { element } = useClassnames(CLASSNAME); const { anchorRef, handleKeyboardNav, @@ -87,13 +86,8 @@ export const SelectMultipleField: React.FC = (props) => { {variant === SelectVariant.input && ( <> {label && ( -
- +
+ {label}
@@ -103,7 +97,7 @@ export const SelectMultipleField: React.FC = (props) => {
, selectElementRef)} id={id} - className={`${CLASSNAME}__wrapper`} + className={element('wrapper')} onClick={onInputClick} onKeyDown={handleKeyboardNav} tabIndex={isDisabled ? undefined : 0} @@ -112,36 +106,31 @@ export const SelectMultipleField: React.FC = (props) => { > {icon && ( )} -
+
{!isEmpty && value.map((val, index) => selectedChipRender?.(val, index, onClear, isDisabled, theme))}
{isEmpty && placeholder && ( -
+
{placeholder}
)} {(isValid || hasError) && ( -
+
)} -
+
@@ -183,17 +172,17 @@ export const SelectMultipleField: React.FC = (props) => { * @return React element. */ export const SelectMultiple = forwardRef((props, ref) => { + const { block } = useClassnames(CLASSNAME); return WithSelectContext( SelectMultipleField, { ...DEFAULT_PROPS, ...props, - className: classNames.join( + className: block( + { + 'has-multiple': !props.isEmpty, + }, props.className, - handleBasicClasses({ - hasMultiple: !props.isEmpty, - prefix: CLASSNAME, - }), ), isEmpty: props.value.length === 0, isMultiple: true, diff --git a/packages/lumx-react/src/components/select/WithSelectContext.tsx b/packages/lumx-react/src/components/select/WithSelectContext.tsx index b9bb7c58b..796247224 100644 --- a/packages/lumx-react/src/components/select/WithSelectContext.tsx +++ b/packages/lumx-react/src/components/select/WithSelectContext.tsx @@ -6,9 +6,8 @@ import { Dropdown } from '@lumx/react/components/dropdown/Dropdown'; import { InputHelper } from '@lumx/react/components/input-helper/InputHelper'; import { useFocusTrap } from '@lumx/react/hooks/useFocusTrap'; import { useListenFocus } from '@lumx/react/hooks/useListenFocus'; -import { handleBasicClasses } from '@lumx/core/js/utils/_internal/className'; +import { useClassnames } from '@lumx/react/utils'; import type { LumxClassName } from '@lumx/core/js/types'; -import { classNames } from '@lumx/core/js/utils'; import { mergeRefs } from '@lumx/react/utils/react/mergeRefs'; import { useId } from '@lumx/react/hooks/useId'; @@ -27,6 +26,7 @@ export const WithSelectContext = ( ref: Ref, ): React.ReactElement => { const defaultTheme = useTheme() || Theme.light; + const { block, element } = useClassnames(CLASSNAME); const { children, className, @@ -84,21 +84,20 @@ export const WithSelectContext = ( return (
{hasError && error && ( - + {error} )} {helper && ( - + {helper} )} diff --git a/packages/lumx-react/src/components/side-navigation/SideNavigation.test.tsx b/packages/lumx-react/src/components/side-navigation/SideNavigation.test.tsx index a0f732a22..f8219afdf 100644 --- a/packages/lumx-react/src/components/side-navigation/SideNavigation.test.tsx +++ b/packages/lumx-react/src/components/side-navigation/SideNavigation.test.tsx @@ -24,7 +24,7 @@ describe(`<${SideNavigation.displayName}>`, () => { it('should render dark theme', () => { const { sideNavigation } = setup({ theme: Theme.dark }); - expect(sideNavigation.className).toMatchInlineSnapshot('"lumx-color-font-light-N lumx-side-navigation"'); + expect(sideNavigation.className).toMatchInlineSnapshot(`"lumx-side-navigation lumx-color-font-light-N"`); }); // Common tests suite. diff --git a/packages/lumx-react/src/components/side-navigation/SideNavigation.tsx b/packages/lumx-react/src/components/side-navigation/SideNavigation.tsx index 4ba2f2079..8832cf12c 100644 --- a/packages/lumx-react/src/components/side-navigation/SideNavigation.tsx +++ b/packages/lumx-react/src/components/side-navigation/SideNavigation.tsx @@ -2,8 +2,9 @@ import { Children, ReactNode } from 'react'; import { SideNavigationItem, Theme } from '@lumx/react'; import { GenericProps, HasTheme, isComponent } from '@lumx/react/utils/type'; -import type { LumxClassName } from '@lumx/core/js/types'; import { classNames } from '@lumx/core/js/utils'; +import { useClassnames } from '@lumx/react/utils'; +import type { LumxClassName } from '@lumx/core/js/types'; import { useTheme } from '@lumx/react/utils/theme/ThemeContext'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; @@ -35,13 +36,14 @@ const CLASSNAME: LumxClassName = 'lumx-side-navigation'; export const SideNavigation = forwardRef((props, ref) => { const defaultTheme = useTheme(); const { children, className, theme = defaultTheme, ...forwardedProps } = props; + const { block } = useClassnames(CLASSNAME); const content = Children.toArray(children).filter(isComponent(SideNavigationItem)); return (
    {content}
diff --git a/packages/lumx-react/src/components/side-navigation/SideNavigationItem.tsx b/packages/lumx-react/src/components/side-navigation/SideNavigationItem.tsx index 9b0c287dd..ca3db1af1 100644 --- a/packages/lumx-react/src/components/side-navigation/SideNavigationItem.tsx +++ b/packages/lumx-react/src/components/side-navigation/SideNavigationItem.tsx @@ -5,12 +5,11 @@ import isEmpty from 'lodash/isEmpty'; import { mdiChevronDown, mdiChevronUp } from '@lumx/icons'; import { Emphasis, Icon, Size, IconButton, IconButtonProps } from '@lumx/react'; import { GenericProps, HasCloseMode, isComponent } from '@lumx/react/utils/type'; -import { handleBasicClasses } from '@lumx/core/js/utils/_internal/className'; import type { LumxClassName } from '@lumx/core/js/types'; -import { classNames } from '@lumx/core/js/utils'; import { useId } from '@lumx/react/hooks/useId'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; import { RawClickable } from '@lumx/react/utils/react/RawClickable'; +import { useClassnames } from '@lumx/react/utils'; /** * Defines the props of the component. @@ -88,6 +87,7 @@ export const SideNavigationItem = forwardRef {shouldSplitActions ? ( -
+
- {icon && } + {icon && } {label} - {icon && } + {icon && } {label} {hasContent && ( @@ -154,7 +153,7 @@ export const SideNavigationItem = forwardRef +
    {content}
)} diff --git a/packages/lumx-react/src/components/skeleton/SkeletonCircle.tsx b/packages/lumx-react/src/components/skeleton/SkeletonCircle.tsx index fd82de275..e85d6d6bb 100644 --- a/packages/lumx-react/src/components/skeleton/SkeletonCircle.tsx +++ b/packages/lumx-react/src/components/skeleton/SkeletonCircle.tsx @@ -1,8 +1,7 @@ import { GlobalSize, Theme, ColorPalette } from '@lumx/react'; import { GenericProps, HasTheme } from '@lumx/react/utils/type'; -import { handleBasicClasses } from '@lumx/core/js/utils/_internal/className'; +import { useClassnames } from '@lumx/react/utils'; import type { LumxClassName } from '@lumx/core/js/types'; -import { classNames } from '@lumx/core/js/utils'; import { useTheme } from '@lumx/react/utils/theme/ThemeContext'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; @@ -38,12 +37,20 @@ const CLASSNAME: LumxClassName = 'lumx-skeleton-circle'; export const SkeletonCircle = forwardRef((props, ref) => { const defaultTheme = useTheme() || Theme.light; const { className, size, color, theme = defaultTheme, ...forwardedProps } = props; + const { block } = useClassnames(CLASSNAME); return (
); }); diff --git a/packages/lumx-react/src/components/skeleton/SkeletonRectangle.tsx b/packages/lumx-react/src/components/skeleton/SkeletonRectangle.tsx index 05cdfe332..c17e41fe7 100644 --- a/packages/lumx-react/src/components/skeleton/SkeletonRectangle.tsx +++ b/packages/lumx-react/src/components/skeleton/SkeletonRectangle.tsx @@ -1,8 +1,7 @@ import { AspectRatio, GlobalSize, Theme, ColorPalette } from '@lumx/react'; import { GenericProps, HasTheme, ValueOf } from '@lumx/react/utils/type'; -import { handleBasicClasses } from '@lumx/core/js/utils/_internal/className'; +import { useClassnames } from '@lumx/react/utils'; import type { LumxClassName } from '@lumx/core/js/types'; -import { classNames } from '@lumx/core/js/utils'; import { useTheme } from '@lumx/react/utils/theme/ThemeContext'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; @@ -61,25 +60,25 @@ export const SkeletonRectangle = forwardRef -
+
); }); diff --git a/packages/lumx-react/src/components/skeleton/SkeletonTypography.tsx b/packages/lumx-react/src/components/skeleton/SkeletonTypography.tsx index 3ec0259f5..38fd39b79 100644 --- a/packages/lumx-react/src/components/skeleton/SkeletonTypography.tsx +++ b/packages/lumx-react/src/components/skeleton/SkeletonTypography.tsx @@ -2,9 +2,8 @@ import { CSSProperties } from 'react'; import { Theme, TypographyInterface, ColorPalette } from '@lumx/react'; import { GenericProps, HasTheme } from '@lumx/react/utils/type'; -import { handleBasicClasses } from '@lumx/core/js/utils/_internal/className'; +import { useClassnames } from '@lumx/react/utils'; import type { LumxClassName } from '@lumx/core/js/types'; -import { classNames } from '@lumx/core/js/utils'; import { useTheme } from '@lumx/react/utils/theme/ThemeContext'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; @@ -42,15 +41,23 @@ const CLASSNAME: LumxClassName = 'lumx-skeleton-typograph export const SkeletonTypography = forwardRef((props, ref) => { const defaultTheme = useTheme() || Theme.light; const { className, theme = defaultTheme, typography, width, color, ...forwardedProps } = props; + const { block, element } = useClassnames(CLASSNAME); return (
-
+
); }); diff --git a/packages/lumx-react/src/components/slider/Slider.tsx b/packages/lumx-react/src/components/slider/Slider.tsx index ae6e58468..b1e60156d 100644 --- a/packages/lumx-react/src/components/slider/Slider.tsx +++ b/packages/lumx-react/src/components/slider/Slider.tsx @@ -4,9 +4,8 @@ import { SyntheticEvent, useMemo, useRef } from 'react'; import { InputHelper, InputLabel, Theme } from '@lumx/react'; import useEventCallback from '@lumx/react/hooks/useEventCallback'; import { GenericProps, HasTheme } from '@lumx/react/utils/type'; -import { handleBasicClasses } from '@lumx/core/js/utils/_internal/className'; +import { useClassnames } from '@lumx/react/utils'; import type { LumxClassName } from '@lumx/core/js/types'; -import { classNames } from '@lumx/core/js/utils'; import { clamp } from '@lumx/react/utils/number/clamp'; import { useId } from '@lumx/react/hooks/useId'; import { useTheme } from '@lumx/react/utils/theme/ThemeContext'; @@ -111,6 +110,7 @@ export const Slider = forwardRef((props, ref) => { value, ...forwardedProps } = otherProps; + const { block, element } = useClassnames(CLASSNAME); const generatedId = useId(); const sliderId = id || generatedId; const sliderLabelId = useMemo(() => `label-${sliderId}`, [sliderId]); @@ -237,40 +237,38 @@ export const Slider = forwardRef((props, ref) => {
{label && ( - + {label} )} {helper && ( - + {helper} )} -
- {!hideMinMaxLabel && ( - {min} - )} -
-
-
+
+ {!hideMinMaxLabel && {min}} +
+
+
{steps ? ( -
+
{availableSteps.map((step, idx) => (
))} @@ -281,15 +279,13 @@ export const Slider = forwardRef((props, ref) => { aria-labelledby={sliderLabelId} name={name} id={sliderId} - className={`${CLASSNAME}__handle`} + className={element('handle')} style={{ left: percentString }} onKeyDown={isAnyDisabled ? undefined : handleKeyDown} {...disabledStateProps} />
- {!hideMinMaxLabel && ( - {max} - )} + {!hideMinMaxLabel && {max}}
); diff --git a/packages/lumx-react/src/components/slideshow/Slides.tsx b/packages/lumx-react/src/components/slideshow/Slides.tsx index 78e778d22..e6c222b73 100644 --- a/packages/lumx-react/src/components/slideshow/Slides.tsx +++ b/packages/lumx-react/src/components/slideshow/Slides.tsx @@ -4,9 +4,8 @@ import chunk from 'lodash/chunk'; import { FULL_WIDTH_PERCENT } from '@lumx/react/components/slideshow/constants'; import { GenericProps, HasTheme } from '@lumx/react/utils/type'; -import { handleBasicClasses } from '@lumx/core/js/utils/_internal/className'; +import { useClassnames } from '@lumx/react/utils'; import type { LumxClassName } from '@lumx/core/js/types'; -import { classNames } from '@lumx/core/js/utils'; import { useTheme } from '@lumx/react/utils/theme/ThemeContext'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; @@ -80,6 +79,7 @@ export const Slides = forwardRef((props, ref) => { const wrapperRef = React.useRef(null); const startIndexVisible = activeIndex; const endIndexVisible = startIndexVisible + 1; + const { block, element } = useClassnames(CLASSNAME); // Inline style of wrapper element. const wrapperStyle: CSSProperties = { transform: `translateX(-${FULL_WIDTH_PERCENT * activeIndex}%)` }; @@ -94,20 +94,24 @@ export const Slides = forwardRef((props, ref) => { id={id} ref={ref} {...forwardedProps} - className={classNames.join(className, handleBasicClasses({ prefix: CLASSNAME, theme }), { - [`${CLASSNAME}--fill-height`]: fillHeight, - [`${CLASSNAME}--group-by-${groupBy}`]: Boolean(groupBy), - })} + className={block( + { + [`theme-${theme}`]: Boolean(theme), + 'fill-height': fillHeight, + [`group-by-${groupBy}`]: Boolean(groupBy), + }, + className, + )} aria-roledescription="carousel" >
-
+
{groups.map((group, index) => ( { - element.click(); + onElementFocus: (el) => { + el.click(); }, }); @@ -140,25 +140,24 @@ const InternalSlideshowControls = forwardRef PAGINATION_ITEMS_MAX, - })} + className={block( + { + [`theme-${theme}`]: Boolean(theme), + 'has-infinite-pagination': slidesCount > PAGINATION_ITEMS_MAX, + }, + className, + )} > -
-
+
+
{useMemo( () => range(slidesCount).map((index) => { @@ -179,13 +178,13 @@ const InternalSlideshowControls = forwardRef @@ -225,7 +225,7 @@ const InternalSlideshowControls = forwardRef = 'lumx-slideshow-item'; */ export const SlideshowItem = forwardRef((props, ref) => { const { className, children, ...forwardedProps } = props; + const { block } = useClassnames(CLASSNAME); return ( -
+
{children}
); diff --git a/packages/lumx-react/src/components/slideshow/SlideshowItemGroup.tsx b/packages/lumx-react/src/components/slideshow/SlideshowItemGroup.tsx index d8c48a83f..8eebea1c0 100644 --- a/packages/lumx-react/src/components/slideshow/SlideshowItemGroup.tsx +++ b/packages/lumx-react/src/components/slideshow/SlideshowItemGroup.tsx @@ -3,8 +3,8 @@ import React from 'react'; import { mergeRefs } from '@lumx/react/utils/react/mergeRefs'; import { GenericProps } from '@lumx/react/utils/type'; import type { LumxClassName } from '@lumx/core/js/types'; -import { classNames } from '@lumx/core/js/utils'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; +import { useClassnames } from '@lumx/react/utils'; import { useSlideFocusManagement } from './useSlideFocusManagement'; /** @@ -38,6 +38,7 @@ export const buildSlideShowGroupId = (slidesId: string, index: number) => `${sli */ export const SlideshowItemGroup = forwardRef((props, ref) => { const { className, children, role = 'group', label, isDisplayed, ...forwardedProps } = props; + const { block } = useClassnames(CLASSNAME); const groupRef = React.useRef(null); useSlideFocusManagement({ isSlideDisplayed: isDisplayed, slideRef: groupRef }); @@ -46,7 +47,7 @@ export const SlideshowItemGroup = forwardRef((props, ref) => { inputProps = {}, ...forwardedProps } = otherProps; + const { block, element } = useClassnames(CLASSNAME); const generatedInputId = useId(); const inputId = id || generatedInputId; const handleChange = (event: React.ChangeEvent) => { @@ -91,24 +89,23 @@ export const Switch = forwardRef((props, ref) => {
-
+
((props, ref) => { {...inputProps} /> -
-
-
+
+
+
{Children.count(children) > 0 && ( -
- +
+ {children} - {!isEmpty(helper) && ( - + {helper && ( + {helper} )} diff --git a/packages/lumx-react/src/components/table/Table.tsx b/packages/lumx-react/src/components/table/Table.tsx index b3bd6966a..180477002 100644 --- a/packages/lumx-react/src/components/table/Table.tsx +++ b/packages/lumx-react/src/components/table/Table.tsx @@ -1,7 +1,6 @@ import { Theme } from '@lumx/react'; import { GenericProps, HasTheme } from '@lumx/react/utils/type'; -import { handleBasicClasses } from '@lumx/core/js/utils/_internal/className'; -import { classNames } from '@lumx/core/js/utils'; +import { useClassnames } from '@lumx/react/utils'; import { useTheme } from '@lumx/react/utils/theme/ThemeContext'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; @@ -34,14 +33,19 @@ const DEFAULT_PROPS: Partial = {}; export const Table = forwardRef((props, ref) => { const defaultTheme = useTheme() || Theme.light; const { children, className, hasBefore, hasDividers, theme = defaultTheme, ...forwardedProps } = props; + const { block } = useClassnames(CLASSNAME); return ( {children} diff --git a/packages/lumx-react/src/components/table/TableBody.tsx b/packages/lumx-react/src/components/table/TableBody.tsx index 6354fb44e..50b2e6dfc 100644 --- a/packages/lumx-react/src/components/table/TableBody.tsx +++ b/packages/lumx-react/src/components/table/TableBody.tsx @@ -1,6 +1,6 @@ import { GenericProps } from '@lumx/react/utils/type'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; -import { classNames } from '@lumx/core/js/utils'; +import { useClassnames } from '@lumx/react/utils'; import { CLASSNAME as TABLE_CLASSNAME } from './constants'; @@ -31,9 +31,10 @@ const CLASSNAME = `${TABLE_CLASSNAME}__body`; */ export const TableBody = forwardRef((props, ref) => { const { children, className, ...forwardedProps } = props; + const { block } = useClassnames(CLASSNAME); return ( - + {children} ); diff --git a/packages/lumx-react/src/components/table/TableCell.tsx b/packages/lumx-react/src/components/table/TableCell.tsx index 1c36e794a..c25d145cf 100644 --- a/packages/lumx-react/src/components/table/TableCell.tsx +++ b/packages/lumx-react/src/components/table/TableCell.tsx @@ -1,9 +1,8 @@ import { mdiArrowDown, mdiArrowUp } from '@lumx/icons'; import { Icon, Size } from '@lumx/react'; import { GenericProps, ValueOf } from '@lumx/react/utils/type'; -import { handleBasicClasses } from '@lumx/core/js/utils/_internal/className'; -import { classNames } from '@lumx/core/js/utils'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; +import { useClassnames } from '@lumx/react/utils'; import { CLASSNAME as TABLE_CLASSNAME } from './constants'; @@ -73,6 +72,8 @@ export const TableCell = forwardRef((props ...forwardedProps } = props; + const { block } = useClassnames(CLASSNAME); + // Use button if clickable const Wrapper = onHeaderClick ? 'button' : 'div'; const wrapperProps = Wrapper === 'button' ? ({ type: 'button', onClick: onHeaderClick } as const) : undefined; @@ -91,14 +92,13 @@ export const TableCell = forwardRef((props )} diff --git a/packages/lumx-react/src/components/table/TableHeader.tsx b/packages/lumx-react/src/components/table/TableHeader.tsx index 425ca203b..ef2f8e4dd 100644 --- a/packages/lumx-react/src/components/table/TableHeader.tsx +++ b/packages/lumx-react/src/components/table/TableHeader.tsx @@ -1,6 +1,6 @@ import { GenericProps } from '@lumx/react/utils/type'; -import { classNames } from '@lumx/core/js/utils'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; +import { useClassnames } from '@lumx/react/utils'; import { CLASSNAME as TABLE_CLASSNAME } from './constants'; @@ -36,9 +36,10 @@ const DEFAULT_PROPS: Partial = {}; */ export const TableHeader = forwardRef((props, ref) => { const { children, className, ...forwardedProps } = props; + const { block } = useClassnames(CLASSNAME); return ( - + {children} ); diff --git a/packages/lumx-react/src/components/table/TableRow.tsx b/packages/lumx-react/src/components/table/TableRow.tsx index bf0eaa5ad..8f015253b 100644 --- a/packages/lumx-react/src/components/table/TableRow.tsx +++ b/packages/lumx-react/src/components/table/TableRow.tsx @@ -1,8 +1,7 @@ import { GenericProps } from '@lumx/react/utils/type'; -import { handleBasicClasses } from '@lumx/core/js/utils/_internal/className'; -import { classNames } from '@lumx/core/js/utils'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; import { useDisableStateProps } from '@lumx/react/utils/disabled/useDisableStateProps'; +import { useClassnames } from '@lumx/react/utils'; import { CLASSNAME as TABLE_CLASSNAME } from './constants'; @@ -45,20 +44,21 @@ const DEFAULT_PROPS: Partial = {}; export const TableRow = forwardRef((props, ref) => { const { isAnyDisabled, disabledStateProps, otherProps } = useDisableStateProps(props); const { children, className, isClickable, isSelected, ...forwardedProps } = otherProps; + const { element } = useClassnames(TABLE_CLASSNAME); return ( diff --git a/packages/lumx-react/src/components/tabs/Tab.tsx b/packages/lumx-react/src/components/tabs/Tab.tsx index 7961958a4..9307760ad 100644 --- a/packages/lumx-react/src/components/tabs/Tab.tsx +++ b/packages/lumx-react/src/components/tabs/Tab.tsx @@ -2,8 +2,7 @@ import { FocusEventHandler, KeyboardEventHandler, ReactNode, useCallback } from import { Icon, IconProps, Size, Text } from '@lumx/react'; import { GenericProps } from '@lumx/react/utils/type'; -import { handleBasicClasses } from '@lumx/core/js/utils/_internal/className'; -import { classNames } from '@lumx/core/js/utils'; +import { useClassnames } from '@lumx/react/utils'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; import { useDisableStateProps } from '@lumx/react/utils/disabled/useDisableStateProps'; @@ -70,6 +69,7 @@ export const Tab = forwardRef((props, ref) => { } = otherProps; const state = useTabProviderContext('tab', id); const isActive = propIsActive || state?.isActive; + const { element } = useClassnames('lumx-tabs'); const changeToCurrentTab = useCallback(() => { if (isAnyDisabled) { @@ -105,9 +105,13 @@ export const Tab = forwardRef((props, ref) => { {...forwardedProps} type="button" id={state?.tabId} - className={classNames.join( + className={element( + 'link', + { + 'is-active': isActive, + 'is-disabled': isAnyDisabled, + }, className, - handleBasicClasses({ prefix: CLASSNAME, isActive, isDisabled: isAnyDisabled }), )} onClick={changeToCurrentTab} onKeyPress={handleKeyPress} diff --git a/packages/lumx-react/src/components/tabs/TabList.tsx b/packages/lumx-react/src/components/tabs/TabList.tsx index dee811d91..be3e712b4 100644 --- a/packages/lumx-react/src/components/tabs/TabList.tsx +++ b/packages/lumx-react/src/components/tabs/TabList.tsx @@ -2,11 +2,10 @@ import React, { ReactNode } from 'react'; import { Alignment, Theme } from '@lumx/react'; import { GenericProps, HasTheme } from '@lumx/react/utils/type'; -import { handleBasicClasses } from '@lumx/core/js/utils/_internal/className'; -import { classNames } from '@lumx/core/js/utils'; import { mergeRefs } from '@lumx/react/utils/react/mergeRefs'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; import { useTheme } from '@lumx/react/utils/theme/ThemeContext'; +import { useClassnames } from '@lumx/react/utils'; import { useRovingTabIndex } from '../../hooks/useRovingTabIndex'; import { TABS_CLASSNAME as CLASSNAME } from './constants'; @@ -64,6 +63,7 @@ export const TabList = forwardRef((props, ref) => ...forwardedProps } = props; const tabListRef = React.useRef(null); + const { block, element } = useClassnames(CLASSNAME); useRovingTabIndex({ parentRef: tabListRef, elementSelector: '[role="tab"]', @@ -75,9 +75,16 @@ export const TabList = forwardRef((props, ref) =>
-
+
{children}
diff --git a/packages/lumx-react/src/components/tabs/TabPanel.tsx b/packages/lumx-react/src/components/tabs/TabPanel.tsx index 4f7fab87d..a2e30324b 100644 --- a/packages/lumx-react/src/components/tabs/TabPanel.tsx +++ b/packages/lumx-react/src/components/tabs/TabPanel.tsx @@ -1,8 +1,7 @@ import { forwardRef } from '@lumx/react/utils/react/forwardRef'; import { useTabProviderContext } from '@lumx/react/components/tabs/state'; import { GenericProps, LumxClassName } from '@lumx/react/utils/type'; -import { handleBasicClasses } from '@lumx/core/js/utils/_internal/className'; -import { classNames } from '@lumx/core/js/utils'; +import { useClassnames } from '@lumx/react/utils'; /** * Defines the props of the component. @@ -45,13 +44,19 @@ export const TabPanel = forwardRef((props, ref) = const state = useTabProviderContext('tabPanel', id); const isActive = propIsActive || state?.isActive; + const { block } = useClassnames(CLASSNAME); return (
void; } + /** * Component default props. */ @@ -42,6 +42,7 @@ export const RawInputText = forwardRef((pro ...forwardedProps } = props; const textareaRef = useRef(null); + const { block } = useClassnames(INPUT_NATIVE_CLASSNAME); const handleChange: ChangeEventHandler = useCallback( (evt) => { @@ -56,10 +57,12 @@ export const RawInputText = forwardRef((pro name={name} type={type} ref={useMergeRefs(ref, textareaRef)} - className={classNames.join( - className, - handleBasicClasses({ prefix: INPUT_NATIVE_CLASSNAME, theme }), - `${INPUT_NATIVE_CLASSNAME}--text`, + className={block( + { + [`theme-${theme}`]: Boolean(theme), + text: true, + }, + [className], )} onChange={handleChange} value={value} diff --git a/packages/lumx-react/src/components/text-field/RawInputTextarea.tsx b/packages/lumx-react/src/components/text-field/RawInputTextarea.tsx index 03f607140..96afd5c8b 100644 --- a/packages/lumx-react/src/components/text-field/RawInputTextarea.tsx +++ b/packages/lumx-react/src/components/text-field/RawInputTextarea.tsx @@ -4,8 +4,7 @@ import { Theme, useTheme } from '@lumx/react'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; import { useMergeRefs } from '@lumx/react/utils/react/mergeRefs'; import type { HasClassName, HasTheme } from '@lumx/core/js/types'; -import { handleBasicClasses } from '@lumx/core/js/utils/_internal/className'; -import { classNames } from '@lumx/core/js/utils'; +import { useClassnames } from '@lumx/react/utils'; import { useFitRowsToContent } from './useFitRowsToContent'; import { INPUT_NATIVE_CLASSNAME } from './constants'; @@ -44,8 +43,8 @@ export const RawInputTextarea = forwardRef, ...forwardedProps } = props; const textareaRef = useRef(null); - const rows = useFitRowsToContent(minimumRows, textareaRef, value); + const { block } = useClassnames(INPUT_NATIVE_CLASSNAME); const handleChange: ChangeEventHandler = useCallback( (evt) => { @@ -58,10 +57,12 @@ export const RawInputTextarea = forwardRef,
@@ -119,7 +119,7 @@ export const TableCell = forwardRef((props )} {variant === TableCellVariant.body && ( - +
{children}