From afb156b5fd858a2ccd9a829f1497d09c40b69ae1 Mon Sep 17 00:00:00 2001 From: "codegen-sh[bot]" <131295404+codegen-sh[bot]@users.noreply.github.com> Date: Thu, 13 Nov 2025 00:21:55 +0000 Subject: [PATCH 1/5] feat: integrate shadcn/ui InputGroup component for prefix/suffix functionality - Add official shadcn/ui InputGroup component with proper accessibility - Refactor TextField to use InputGroup when prefix/suffix props are provided - Maintain backward compatibility with existing TextField API - Deprecate legacy FieldPrefix/FieldSuffix components (kept for compatibility) - Add biome-ignore comments for intentional WAI-ARIA role usage - All existing tests pass with the new implementation Benefits: - Cleaner, more maintainable code structure - Better accessibility with proper semantic markup - Official shadcn/ui patterns and styling - Support for inline-start, inline-end, block-start, block-end alignment - No breaking changes - existing code continues to work Requested by: Jake Ruesink --- packages/components/package.json | 2 +- packages/components/src/ui/index.ts | 1 + packages/components/src/ui/input-group.tsx | 152 +++++++++++++++++++++ packages/components/src/ui/text-field.tsx | 52 ++++--- yarn.lock | 19 ++- 5 files changed, 204 insertions(+), 22 deletions(-) create mode 100644 packages/components/src/ui/input-group.tsx diff --git a/packages/components/package.json b/packages/components/package.json index be7da4d3..f43ba8eb 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -58,7 +58,7 @@ "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.6", "@radix-ui/react-slider": "^1.3.4", - "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-tabs": "^1.1.11", "@radix-ui/react-tooltip": "^1.1.6", diff --git a/packages/components/src/ui/index.ts b/packages/components/src/ui/index.ts index 8e2550a8..bb9c48ba 100644 --- a/packages/components/src/ui/index.ts +++ b/packages/components/src/ui/index.ts @@ -13,6 +13,7 @@ export * from './date-picker-field'; export * from './dropdown-menu'; export * from './form'; export * from './form-error-field'; +export * from './input-group'; export * from './label'; export * from './otp-input'; export * from './otp-input-field'; diff --git a/packages/components/src/ui/input-group.tsx b/packages/components/src/ui/input-group.tsx new file mode 100644 index 00000000..b859b62f --- /dev/null +++ b/packages/components/src/ui/input-group.tsx @@ -0,0 +1,152 @@ +'use client'; + +import type * as React from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; + +import { cn } from './utils'; +import { Button } from './button'; +import { TextInput } from './text-input'; +import { Textarea } from './textarea'; + +function InputGroup({ className, ...props }: React.ComponentProps<'div'>) { + return ( + // biome-ignore lint/a11y/useSemanticElements: role="group" is appropriate for input groups per WAI-ARIA +
textarea]:h-auto', + + // Variants based on alignment. + 'has-[>[data-align=inline-start]]:[&>input]:pl-2', + 'has-[>[data-align=inline-end]]:[&>input]:pr-2', + 'has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3', + 'has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3', + + // Focus state. + 'has-[[data-slot=input-group-control]:focus-visible]:ring-ring has-[[data-slot=input-group-control]:focus-visible]:ring-1', + + // Error state. + 'has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40', + + className, + )} + {...props} + /> + ); +} + +const inputGroupAddonVariants = cva( + "text-muted-foreground flex h-auto cursor-text select-none items-center justify-center gap-2 py-1.5 text-sm font-medium group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4", + { + variants: { + align: { + 'inline-start': 'order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]', + 'inline-end': 'order-last pr-3 has-[>button]:mr-[-0.4rem] has-[>kbd]:mr-[-0.35rem]', + 'block-start': + '[.border-b]:pb-3 order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5', + 'block-end': '[.border-t]:pt-3 order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5', + }, + }, + defaultVariants: { + align: 'inline-start', + }, + }, +); + +function InputGroupAddon({ + className, + align = 'inline-start', + ...props +}: React.ComponentProps<'div'> & VariantProps) { + return ( + // biome-ignore lint/a11y/useSemanticElements: role="group" is appropriate for input group addons per WAI-ARIA + // biome-ignore lint/a11y/useKeyWithClickEvents: onClick is for focus management, not interactive action +
{ + if ((e.target as HTMLElement).closest('button')) { + return; + } + e.currentTarget.parentElement?.querySelector('input')?.focus(); + }} + {...props} + /> + ); +} + +const inputGroupButtonVariants = cva('flex items-center gap-2 text-sm shadow-none', { + variants: { + size: { + xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5", + sm: 'h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5', + 'icon-xs': 'size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0', + 'icon-sm': 'size-8 p-0 has-[>svg]:p-0', + }, + }, + defaultVariants: { + size: 'xs', + }, +}); + +function InputGroupButton({ + className, + type = 'button', + variant = 'ghost', + size = 'xs', + ...props +}: Omit, 'size'> & VariantProps) { + return ( +