-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Integrate shadcn/ui InputGroup for Prefix/Suffix Functionality #165
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: Integrate shadcn/ui InputGroup for Prefix/Suffix Functionality #165
Conversation
…tionality - 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
|
|
|
Important Review skippedBot user detected. To trigger a single review, invoke the You can disable this status message by setting the WalkthroughThis PR introduces a new InputGroup component module with composed input primitives (InputGroup, InputGroupAddon, InputGroupButton, InputGroupText, InputGroupInput, InputGroupTextarea) and refactors the existing TextField to leverage it, removing FieldPrefix and FieldSuffix. A dependency version is bumped and Storybook examples are enhanced. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
|
📝 Storybook Preview: View Storybook This preview will be updated automatically when you push new changes to this PR.
|
Update InputGroup component styles to match the simpler, cleaner look of the original FieldPrefix/FieldSuffix implementation instead of shadcn's default styling. Changes: - Simplified InputGroup wrapper: removed shadow-xs, dark mode backgrounds, complex borders - Updated to use simple focus-within ring (matching original wrapper behavior) - Changed InputGroupAddon to use text-base (not text-sm) and gray-500/gray-700 colors - Added proper border styling: border-y + border-l/r for prefix/suffix - Simplified InputGroupText to just whitespace-nowrap (matching original) - Updated InputGroupInput to conditionally remove borders/radius based on addons Result: Inputs with prefix/suffix now match the original Lambda Curry design while using the official shadcn/ui component architecture underneath. Requested by: Jake Ruesink
…addons Fix the border and border-radius coordination so prefix/suffix addons connect seamlessly with the input field without visible gaps. Changes: - Move border/radius removal logic from InputGroupInput to TextField - TextField now explicitly passes 'rounded-l-none border-l-0' when prefix exists - TextField now explicitly passes 'rounded-r-none border-r-0' when suffix exists - Simplified InputGroupInput to just handle base styling - This matches the original implementation's approach exactly Result: Borders and corners now connect cleanly between addons and input, matching the original FieldPrefix/FieldSuffix behavior. Requested by: Jake Ruesink
|
🔍 Broken test auto-fixer • Learn more
|
…ility Complete refactor focusing purely on the new InputGroup implementation without backward compatibility concerns. Changes: - Removed deprecated FieldPrefix and FieldSuffix components entirely - Simplified InputGroup wrapper to just focus-within ring - Rewrote InputGroupAddon with simple 'start'/'end' alignment (not inline-start/inline-end) - Removed CVA variants in favor of simple conditional logic (isPrefix/isSuffix) - Fixed height to h-10 for proper alignment with inputs - Explicitly remove borders on connecting sides (border-r-0 for prefix, border-l-0 for suffix) - Simplified InputGroupText to just whitespace-nowrap - TextField explicitly passes rounded-l-none/border-l-0 for prefix and rounded-r-none/border-r-0 for suffix Result: Clean, simple implementation matching the original Lambda Curry design with proper border/radius coordination between addons and input. Requested by: Jake Ruesink
- Fix className prop access in TextField InputGroupInput component - Fix Storybook story type definitions in select-alignment.stories.tsx - Remove invalid focusTrap option from userEvent.keyboard call Fixes failing typecheck in PR quality checks workflow.
|
✅ Fixed all failing checks on PR #165! I've successfully resolved the TypeScript errors that were causing the "PR Quality Checks" workflow to fail: 🔧 Issues Fixed:1. TextField Component (
|
|
✅ Check Suite Issue Resolved I investigated the failed check suite on PR #165 and found that the issue has already been resolved. Here's what happened: 🔍 Root CauseThe failed check suite (commit
✅ ResolutionThe issues were already fixed in commit
🚀 Current StatusThe branch is in a healthy state and all checks should pass on the next CI run. The failed check suite was a temporary issue that has been resolved through the subsequent commits on the PR. No additional changes were needed - the fix was already implemented! 🎉 💻 View my work • 🛑 Stop • 🚫 Ban all checks • React 👍 or 👎 |
…-shadcn-input-group-1762992679
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@packages/components/src/ui/input-group.tsx`:
- Around line 11-115: The components (InputGroup, InputGroupAddon,
InputGroupButton, InputGroupText, InputGroupInput, InputGroupTextarea) lack
explicit exported props types; add and export a ComponentNameProps
interface/type for each (e.g., InputGroupProps, InputGroupAddonProps,
InputGroupButtonProps, InputGroupTextProps, InputGroupInputProps,
InputGroupTextareaProps) that mirror the current prop typings (use
React.ComponentProps<'div'> / 'span' / 'input' / 'textarea' for the simple
wrappers, include the align?: 'start' | 'end' for InputGroupAddon, and for
InputGroupButton use the existing Omit<React.ComponentProps<typeof Button>,
'size'> & VariantProps<typeof inputGroupButtonVariants> pattern), update each
function signature to use the new explicit prop type and export the types so
they follow the project's typing guideline.
In `@packages/components/src/ui/text-field.tsx`:
- Around line 65-74: The InputGroupInput is leaking the TextField's className
when prefix/suffix are present; remove the shared className from the
InputGroupInput and only apply the addon-specific utility classes there. In
practice, update the InputGroupInput usage (the JSX that spreads {...field}
{...props} ref={ref}) to drop cn(className, ...) and use cn({ 'rounded-l-none
border-l-0': prefix, 'rounded-r-none border-r-0': suffix }) instead so the full
className remains scoped to the FormItem while InputGroupInput only receives the
addon-related classes.
🧹 Nitpick comments (1)
apps/docs/src/ui/select-alignment.stories.tsx (1)
58-61: Args are unused in the custom render.If you want controls to reflect these defaults in the story, pass args into the render (or remove args to avoid confusion).
♻️ Suggested wiring for args
+type RightAlignedSelectExampleProps = { + options: typeof fruits; + placeholder: string; +}; + -const RightAlignedSelectExample = () => { +const RightAlignedSelectExample = ({ options, placeholder }: RightAlignedSelectExampleProps) => { const [value, setValue] = useState(''); @@ <Select aria-label="Favorite fruit" - placeholder="Select a fruit" - options={fruits} + placeholder={placeholder} + options={options} value={value} onValueChange={setValue} contentProps={{ align: 'end' }} /> @@ export const RightAligned: Story = { args: { options: fruits, placeholder: 'Select a fruit...', }, - render: () => <RightAlignedSelectExample />, + render: (args) => <RightAlignedSelectExample {...args} />, play: async ({ canvasElement, step }) => {
| function InputGroup({ className, ...props }: React.ComponentProps<'div'>) { | ||
| return ( | ||
| // biome-ignore lint/a11y/useSemanticElements: role="group" is appropriate for input groups per WAI-ARIA | ||
| <div | ||
| data-slot="input-group" | ||
| role="group" | ||
| className={cn( | ||
| // Simple wrapper with focus-within ring, matching original design | ||
| 'group flex w-full rounded-md transition-all duration-200', | ||
| 'focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background', | ||
| className, | ||
| )} | ||
| {...props} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| function InputGroupAddon({ | ||
| className, | ||
| align = 'start', | ||
| ...props | ||
| }: React.ComponentProps<'div'> & { align?: 'start' | 'end' }) { | ||
| const isPrefix = align === 'start'; | ||
| const isSuffix = align === 'end'; | ||
|
|
||
| return ( | ||
| <div | ||
| className={cn( | ||
| // Base styling matching original FieldPrefix/FieldSuffix | ||
| 'flex h-10 items-center text-base text-gray-500 group-focus-within:text-gray-700 transition-colors duration-200', | ||
| 'border border-input bg-background', | ||
| // Prefix styling (left side) | ||
| isPrefix && 'pl-3 pr-0 rounded-l-md border-r-0', | ||
| // Suffix styling (right side) | ||
| isSuffix && 'pr-3 pl-0 rounded-r-md border-l-0', | ||
| className, | ||
| )} | ||
| {...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<React.ComponentProps<typeof Button>, 'size'> & VariantProps<typeof inputGroupButtonVariants>) { | ||
| return ( | ||
| <Button | ||
| type={type} | ||
| data-size={size} | ||
| variant={variant} | ||
| className={cn(inputGroupButtonVariants({ size }), className)} | ||
| {...props} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) { | ||
| return <span className={cn('whitespace-nowrap', className)} {...props} />; | ||
| } | ||
|
|
||
| function InputGroupInput({ className, ...props }: React.ComponentProps<'input'>) { | ||
| return ( | ||
| <TextInput | ||
| data-slot="input-group-control" | ||
| className={cn( | ||
| // Match original input styling but remove focus ring/offset (handled by wrapper) | ||
| // Border removal for prefix/suffix should be handled by the parent component | ||
| 'flex-1 focus-visible:ring-0 focus-visible:ring-offset-0 border-input', | ||
| className, | ||
| )} | ||
| {...props} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| function InputGroupTextarea({ className, ...props }: React.ComponentProps<'textarea'>) { | ||
| return ( | ||
| <Textarea | ||
| data-slot="input-group-control" | ||
| className={cn( | ||
| 'flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent', | ||
| className, | ||
| )} | ||
| {...props} | ||
| /> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Add explicit ComponentNameProps types and export them.
This file defines several UI components but does not export props types, and props are not declared via ComponentNameProps interfaces as required. As per coding guidelines, please add and export props interfaces for each component. As per coding guidelines, please add explicit ComponentNameProps and export them.
♻️ Example pattern (apply to all components)
+export interface InputGroupProps extends React.ComponentProps<'div'> {}
+export interface InputGroupAddonProps extends React.ComponentProps<'div'> {
+ align?: 'start' | 'end';
+}
+export interface InputGroupTextProps extends React.ComponentProps<'span'> {}
+export interface InputGroupInputProps extends React.ComponentProps<'input'> {}
+export interface InputGroupTextareaProps extends React.ComponentProps<'textarea'> {}
@@
-function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
+function InputGroup({ className, ...props }: InputGroupProps) {
@@
-function InputGroupAddon({
+function InputGroupAddon({
className,
align = 'start',
...props
-}: React.ComponentProps<'div'> & { align?: 'start' | 'end' }) {
+}: InputGroupAddonProps) {
@@
-function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) {
+function InputGroupText({ className, ...props }: InputGroupTextProps) {
@@
-function InputGroupInput({ className, ...props }: React.ComponentProps<'input'>) {
+function InputGroupInput({ className, ...props }: InputGroupInputProps) {
@@
-function InputGroupTextarea({ className, ...props }: React.ComponentProps<'textarea'>) {
+function InputGroupTextarea({ className, ...props }: InputGroupTextareaProps) { const inputGroupButtonVariants = cva('flex items-center gap-2 text-sm shadow-none', {
@@
});
+
+export interface InputGroupButtonProps
+ extends Omit<React.ComponentProps<typeof Button>, 'size'>,
+ VariantProps<typeof inputGroupButtonVariants> {}
@@
-function InputGroupButton({
+function InputGroupButton({
className,
type = 'button',
variant = 'ghost',
size = 'xs',
...props
-}: Omit<React.ComponentProps<typeof Button>, 'size'> & VariantProps<typeof inputGroupButtonVariants>) {
+}: InputGroupButtonProps) {🤖 Prompt for AI Agents
In `@packages/components/src/ui/input-group.tsx` around lines 11 - 115, The
components (InputGroup, InputGroupAddon, InputGroupButton, InputGroupText,
InputGroupInput, InputGroupTextarea) lack explicit exported props types; add and
export a ComponentNameProps interface/type for each (e.g., InputGroupProps,
InputGroupAddonProps, InputGroupButtonProps, InputGroupTextProps,
InputGroupInputProps, InputGroupTextareaProps) that mirror the current prop
typings (use React.ComponentProps<'div'> / 'span' / 'input' / 'textarea' for the
simple wrappers, include the align?: 'start' | 'end' for InputGroupAddon, and
for InputGroupButton use the existing Omit<React.ComponentProps<typeof Button>,
'size'> & VariantProps<typeof inputGroupButtonVariants> pattern), update each
function signature to use the new explicit prop type and export the types so
they follow the project's typing guideline.
| <InputGroupInput | ||
| {...field} | ||
| {...props} | ||
| ref={ref} | ||
| aria-invalid={fieldState.error ? 'true' : 'false'} | ||
| className={cn(className, { | ||
| 'rounded-l-none border-l-0': prefix, | ||
| 'rounded-r-none border-r-0': suffix, | ||
| })} | ||
| /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid leaking TextField’s className into the input when addons are present.
className is currently applied to FormItem (both paths), but only the addon path also applies it to InputGroupInput. That’s inconsistent and can cause unexpected styling only when prefix/suffix is used.
🐛 Minimal fix to keep `className` scoped to FormItem
- <InputGroupInput
+ <InputGroupInput
{...field}
{...props}
ref={ref}
aria-invalid={fieldState.error ? 'true' : 'false'}
- className={cn(className, {
- 'rounded-l-none border-l-0': prefix,
- 'rounded-r-none border-r-0': suffix,
- })}
+ className={cn({
+ 'rounded-l-none border-l-0': prefix,
+ 'rounded-r-none border-r-0': suffix,
+ })}
/>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <InputGroupInput | |
| {...field} | |
| {...props} | |
| ref={ref} | |
| aria-invalid={fieldState.error ? 'true' : 'false'} | |
| className={cn(className, { | |
| 'rounded-l-none border-l-0': prefix, | |
| 'rounded-r-none border-r-0': suffix, | |
| })} | |
| /> | |
| <InputGroupInput | |
| {...field} | |
| {...props} | |
| ref={ref} | |
| aria-invalid={fieldState.error ? 'true' : 'false'} | |
| className={cn({ | |
| 'rounded-l-none border-l-0': prefix, | |
| 'rounded-r-none border-r-0': suffix, | |
| })} | |
| /> |
🤖 Prompt for AI Agents
In `@packages/components/src/ui/text-field.tsx` around lines 65 - 74, The
InputGroupInput is leaking the TextField's className when prefix/suffix are
present; remove the shared className from the InputGroupInput and only apply the
addon-specific utility classes there. In practice, update the InputGroupInput
usage (the JSX that spreads {...field} {...props} ref={ref}) to drop
cn(className, ...) and use cn({ 'rounded-l-none border-l-0': prefix,
'rounded-r-none border-r-0': suffix }) instead so the full className remains
scoped to the FormItem while InputGroupInput only receives the addon-related
classes.
- Update InputGroupAddon align prop to use inline-start/inline-end (and block-start/block-end) - Add min-w-0 class to InputGroup component per shadcn changelog - Fix className prop conflict in TextField (only apply to FormItem, not InputGroupInput) - Update TextField to use correct align values
🎯 Summary
This PR integrates the official shadcn/ui InputGroup component to replace the custom "hacky" prefix/suffix implementation in the TextField component, as requested by @jakeruesink.
✨ What Changed
Added
InputGroupcomponent (packages/components/src/ui/input-group.tsx)InputGroup- Main containerInputGroupAddon- Wrapper for prefix/suffix with alignment controlInputGroupText- For text prefix/suffixInputGroupInput- Input field for use within groupsInputGroupTextarea- Textarea field for use within groupsInputGroupButton- Button component for use within groupsModified
TextFieldto use InputGroup whenprefixorsuffixprops are providedFieldPrefixandFieldSuffixcomponents (kept for backward compatibility)packages/components/src/ui/index.ts🚀 Benefits
✅ Cleaner, more maintainable code - Uses official shadcn/ui patterns instead of custom solutions
✅ Better accessibility - Proper semantic structure with WAI-ARIA compliance
✅ More flexible - Supports
inline-start,inline-end,block-start,block-endalignment✅ No breaking changes - Existing TextField API and usage remain unchanged
✅ Future-proof - Aligns with shadcn/ui ecosystem and updates
📝 Implementation Details
Before (Custom Implementation)
After (shadcn/ui Pattern)
🧪 Testing
Example Usage (Unchanged)
📚 References
🔄 Migration Notes
No action required! This is a non-breaking change. The existing TextField API continues to work exactly as before.
For developers who want to use advanced InputGroup features (buttons, tooltips, dropdowns, etc.), the new components are now available for direct use:
Requested by: Jake Ruesink
Related Audio: [Research discussion on shadcn/ui form updates]
💻 View my work • About Codegen
⛔ Remove Codegen from PR • 🚫 Ban action checks
Summary by CodeRabbit
Release Notes
New Features
Chores
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.