From 76e273ef8fb211332612723abbb6a5caee68e4f8 Mon Sep 17 00:00:00 2001 From: Johan Persson Date: Fri, 9 Jan 2026 17:03:24 +0100 Subject: [PATCH] refactor(ui): extract helpers for useDefinition hook Signed-off-by: Johan Persson --- .../ui/src/hooks/useDefinition/helpers.ts | 134 ++++++++++++++++++ packages/ui/src/hooks/useStyles.ts | 111 ++------------- 2 files changed, 144 insertions(+), 101 deletions(-) create mode 100644 packages/ui/src/hooks/useDefinition/helpers.ts diff --git a/packages/ui/src/hooks/useDefinition/helpers.ts b/packages/ui/src/hooks/useDefinition/helpers.ts new file mode 100644 index 0000000000..86436a89a9 --- /dev/null +++ b/packages/ui/src/hooks/useDefinition/helpers.ts @@ -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, + 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, + 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)[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, + }; +} diff --git a/packages/ui/src/hooks/useStyles.ts b/packages/ui/src/hooks/useStyles.ts index cc05f5f362..7bb25be651 100644 --- a/packages/ui/src/hooks/useStyles.ts +++ b/packages/ui/src/hooks/useStyles.ts @@ -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, - 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)[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, };