From 40bb35156b22bfce9481fdced45b314a5fc5c833 Mon Sep 17 00:00:00 2001 From: Johan Persson Date: Fri, 9 Jan 2026 17:14:37 +0100 Subject: [PATCH] feat(ui): implement useDefinition hook Signed-off-by: Johan Persson --- .../src/hooks/useDefinition/useDefinition.tsx | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 packages/ui/src/hooks/useDefinition/useDefinition.tsx diff --git a/packages/ui/src/hooks/useDefinition/useDefinition.tsx b/packages/ui/src/hooks/useDefinition/useDefinition.tsx new file mode 100644 index 0000000000..990be4ad1c --- /dev/null +++ b/packages/ui/src/hooks/useDefinition/useDefinition.tsx @@ -0,0 +1,146 @@ +/* + * 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 { useMemo, ReactNode } from 'react'; +import clsx from 'clsx'; +import { useBreakpoint } from '../useBreakpoint'; +import { useSurface, SurfaceProvider, UseSurfaceOptions } from '../useSurface'; +import { resolveResponsiveValue, processUtilityProps } from './helpers'; +import type { + ComponentConfig, + UseDefinitionOptions, + UseDefinitionResult, +} from './types'; + +export function useDefinition< + D extends ComponentConfig, + P extends Record, +>( + definition: D, + props: P, + options?: UseDefinitionOptions, +): UseDefinitionResult { + const { breakpoint } = useBreakpoint(); + + const surfaceOptions: UseSurfaceOptions | undefined = + definition.surface === 'container' + ? { surface: props.surface } + : definition.surface === 'leaf' + ? { onSurface: props.onSurface } + : undefined; + + const { surface: resolvedSurface } = useSurface(surfaceOptions); + + return useMemo(() => { + // Step 4a: Separate props + const ownPropKeys = new Set(Object.keys(definition.propDefs)); + const utilityPropKeys = new Set(definition.utilityProps ?? []); + + const ownPropsRaw: Record = {}; + const restProps: Record = {}; + + for (const [key, value] of Object.entries(props)) { + if (ownPropKeys.has(key)) { + ownPropsRaw[key] = value; + } else if (!utilityPropKeys.has(key)) { + restProps[key] = value; + } + } + + // Step 4b: Resolve props, apply defaults, generate data attributes + const ownPropsResolved: Record = {}; + const dataAttributes: Record = {}; + + for (const [key, config] of Object.entries(definition.propDefs)) { + const rawValue = ownPropsRaw[key]; + const resolvedValue = resolveResponsiveValue(rawValue, breakpoint); + const finalValue = resolvedValue ?? (config as any).default; + + if (finalValue !== undefined) { + ownPropsResolved[key] = finalValue; + + if ((config as any).dataAttribute) { + dataAttributes[`data-${key}`] = String(finalValue); + } + } + } + + // Add data-on-surface for leaf components + if (definition.surface === 'leaf' && resolvedSurface !== undefined) { + // Handle responsive surface values - for data attributes, use the resolved string + const surfaceValue = + typeof resolvedSurface === 'object' + ? resolveResponsiveValue(resolvedSurface as any, breakpoint) + : resolvedSurface; + if (surfaceValue !== undefined) { + dataAttributes['data-on-surface'] = String(surfaceValue); + } + } + + // Step 4c: Process utility props + const { utilityClasses, utilityStyle } = processUtilityProps( + props, + definition.utilityProps ?? [], + ); + + // Step 4d: Assemble classes + const utilityTarget = options?.utilityTarget ?? 'root'; + const classNameTarget = options?.classNameTarget ?? 'root'; + + const classes: Record = {}; + + for (const [name, cssKey] of Object.entries(definition.classNames)) { + classes[name] = clsx( + cssKey as string, + definition.styles[cssKey as keyof typeof definition.styles], + { + [utilityClasses]: utilityTarget === name, + [ownPropsResolved.className]: classNameTarget === name, + }, + ); + } + + // Step 4e: Handle children / surfaceChildren + let children: ReactNode | undefined; + let surfaceChildren: ReactNode | undefined; + + if (definition.surface === 'container') { + surfaceChildren = resolvedSurface ? ( + + {props.children} + + ) : ( + props.children + ); + } else { + children = props.children; + } + + // Step 4f: Return result + return { + ownProps: { + classes, + ...ownPropsResolved, + ...(definition.surface === 'container' + ? { surfaceChildren } + : { children }), + }, + restProps, + dataAttributes, + utilityStyle, + } as unknown as UseDefinitionResult; + }, [definition, props, breakpoint, resolvedSurface, options]); +}