refactor(ui): extract helpers for useDefinition hook

Signed-off-by: Johan Persson <johanopersson@gmail.com>
This commit is contained in:
Johan Persson
2026-01-09 17:03:24 +01:00
parent 6192ed1f19
commit 76e273ef8f
2 changed files with 144 additions and 101 deletions
@@ -0,0 +1,134 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { CSSProperties } from 'react';
import { breakpoints } from '../useBreakpoint';
import { utilityClassMap } from '../../utils/utilityClassMap';
/**
* Resolve a responsive value based on the current breakpoint
* @param value - The responsive value (string or object with breakpoint keys)
* @param breakpoint - The current breakpoint
* @returns The resolved value for the current breakpoint
*/
export function resolveResponsiveValue(
value: string | Record<string, string>,
breakpoint: string,
): string | undefined {
if (typeof value === 'string') {
return value;
}
if (typeof value === 'object' && value !== null) {
const index = breakpoints.findIndex(b => b.id === breakpoint);
// Look for value at current breakpoint or smaller
for (let i = index; i >= 0; i--) {
if (value[breakpoints[i].id]) {
return value[breakpoints[i].id];
}
}
// If no value found, check from smallest breakpoint up
for (let i = 0; i < breakpoints.length; i++) {
if (value[breakpoints[i].id]) {
return value[breakpoints[i].id];
}
}
}
return undefined;
}
/**
* Process utility props and generate utility classes and styles
* @param props - All component props
* @param utilityPropKeys - Array of utility prop names to process
* @returns Object with utilityClasses string and utilityStyle CSSProperties
*/
export function processUtilityProps(
props: Record<string, any>,
utilityPropKeys: readonly string[],
): { utilityClasses: string; utilityStyle: CSSProperties } {
const utilityClassList: string[] = [];
const generatedStyle: CSSProperties = {};
const handleUtilityValue = (
key: string,
val: unknown,
prefix: string = '',
) => {
// Get utility class configuration for this key
const utilityConfig = utilityClassMap[key as keyof typeof utilityClassMap];
if (!utilityConfig) {
// Skip if no config found for this key
return;
}
// Check if value is in the list of valid values for this utility
if (
utilityConfig.values.length > 0 &&
utilityConfig.values.includes(val as string | number)
) {
// Generate utility class with value suffix and optional breakpoint prefix
const className = prefix
? `${prefix}${utilityConfig.class}-${val}`
: `${utilityConfig.class}-${val}`;
utilityClassList.push(className);
} else if (utilityConfig.cssVar) {
// Custom value - add CSS custom property AND utility class name
// Only if cssVar is defined (properties with fixed values don't have cssVar)
const cssVarKey = prefix
? `${utilityConfig.cssVar}-${prefix.slice(0, -1)}`
: utilityConfig.cssVar;
// CSS custom properties need to be set on the style object as strings
(generatedStyle as Record<string, unknown>)[cssVarKey] = val;
// Add utility class name (without value suffix) with optional breakpoint prefix
const className = prefix
? `${prefix}${utilityConfig.class}`
: utilityConfig.class;
utilityClassList.push(className);
}
// If no cssVar and value is not in valid values, skip (invalid value for fixed-value property)
};
for (const key of utilityPropKeys) {
const value = props[key];
if (value === undefined || value === null) {
continue;
}
// Check if value is a responsive object
if (typeof value === 'object' && value !== null) {
const breakpointValues = value as { [key: string]: unknown };
// Handle responsive object values
for (const bp in breakpointValues) {
const prefix = bp === 'initial' ? '' : `${bp}:`;
handleUtilityValue(key, breakpointValues[bp], prefix);
}
} else {
// Handle simple value
handleUtilityValue(key, value);
}
}
return {
utilityClasses: utilityClassList.join(' '),
utilityStyle: generatedStyle,
};
}
+10 -101
View File
@@ -13,44 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useBreakpoint, breakpoints } from './useBreakpoint';
import { useBreakpoint } from './useBreakpoint';
import type { ComponentDefinition } from '../types';
import { utilityClassMap } from '../utils/utilityClassMap';
/**
* Resolve a responsive value based on the current breakpoint
* @param value - The responsive value (string or object with breakpoint keys)
* @param breakpoint - The current breakpoint
* @returns The resolved value for the current breakpoint
*/
function resolveResponsiveValue(
value: string | Record<string, string>,
breakpoint: string,
): string | undefined {
if (typeof value === 'string') {
return value;
}
if (typeof value === 'object' && value !== null) {
const index = breakpoints.findIndex(b => b.id === breakpoint);
// Look for value at current breakpoint or smaller
for (let i = index; i >= 0; i--) {
if (value[breakpoints[i].id]) {
return value[breakpoints[i].id];
}
}
// If no value found, check from smallest breakpoint up
for (let i = 0; i < breakpoints.length; i++) {
if (value[breakpoints[i].id]) {
return value[breakpoints[i].id];
}
}
}
return undefined;
}
import {
resolveResponsiveValue,
processUtilityProps,
} from './useDefinition/helpers';
/**
* React hook to get class names and data attributes for a component with responsive support
@@ -105,69 +73,10 @@ export function useStyles<
}
// Generate utility classes and custom styles from component's allowed utility props
const utilityClassList: string[] = [];
const generatedStyle: React.CSSProperties = {};
const handleUtilityValue = (
key: string,
val: unknown,
prefix: string = '',
) => {
// Get utility class configuration for this key
const utilityConfig = utilityClassMap[key as keyof typeof utilityClassMap];
if (!utilityConfig) {
// Skip if no config found for this key
return;
}
// Check if value is in the list of valid values for this utility
if (
utilityConfig.values.length > 0 &&
utilityConfig.values.includes(val as string | number)
) {
// Generate utility class with value suffix and optional breakpoint prefix
const className = prefix
? `${prefix}${utilityConfig.class}-${val}`
: `${utilityConfig.class}-${val}`;
utilityClassList.push(className);
} else if (utilityConfig.cssVar) {
// Custom value - add CSS custom property AND utility class name
// Only if cssVar is defined (properties with fixed values don't have cssVar)
const cssVarKey = prefix
? `${utilityConfig.cssVar}-${prefix.slice(0, -1)}`
: utilityConfig.cssVar;
// CSS custom properties need to be set on the style object as strings
(generatedStyle as Record<string, unknown>)[cssVarKey] = val;
// Add utility class name (without value suffix) with optional breakpoint prefix
const className = prefix
? `${prefix}${utilityConfig.class}`
: utilityConfig.class;
utilityClassList.push(className);
}
// If no cssVar and value is not in valid values, skip (invalid value for fixed-value property)
};
for (const key of utilityPropNames) {
const value = props[key];
if (value === undefined || value === null) {
continue;
}
// Check if value is a responsive object
if (typeof value === 'object' && value !== null) {
const breakpointValues = value as { [key: string]: unknown };
// Handle responsive object values
for (const bp in breakpointValues) {
const prefix = bp === 'initial' ? '' : `${bp}:`;
handleUtilityValue(key, breakpointValues[bp], prefix);
}
} else {
// Handle simple value
handleUtilityValue(key, value);
}
}
const { utilityClasses, utilityStyle: generatedStyle } = processUtilityProps(
props,
utilityPropNames,
);
// Create cleaned props by excluding only utility props
// All other props (including data attributes, style, children, etc.) remain
@@ -195,7 +104,7 @@ export function useStyles<
return {
classNames,
dataAttributes,
utilityClasses: utilityClassList.join(' '),
utilityClasses,
style: mergedStyle,
cleanedProps,
};