Skip to content

feat: Textfield prefix followup#10014

Open
snowystinger wants to merge 6 commits intomainfrom
textfield-prefix-followup
Open

feat: Textfield prefix followup#10014
snowystinger wants to merge 6 commits intomainfrom
textfield-prefix-followup

Conversation

@snowystinger
Copy link
Copy Markdown
Member

Closes

Team talked about supporting more things in the prefix slot today.

It center baselines everything automatically for alignment. Should it also set size on avatar and swatch? that's more things that each field has to pull in.

This also adds an id to associate with the input's aria-labelledby so it's announced when a user arrives in an input.

✅ Pull Request Checklist:

  • Included link to corresponding React Spectrum GitHub Issue.
  • Added/updated unit tests and storybook for this change (for new code or code which already has tests).
  • Filled out test instructions.
  • Updated documentation (if it already exists for this component).
  • Looked at the Accessibility Practices for this feature - Aria Practices

📝 Test Instructions:

🧢 Your Project:

@rspbot
Copy link
Copy Markdown

rspbot commented May 5, 2026

@rspbot
Copy link
Copy Markdown

rspbot commented May 5, 2026

## API Changes

@react-spectrum/s2

/@react-spectrum/s2:ColorField

 ColorField {
   UNSAFE_className?: UnsafeClassName
   UNSAFE_style?: CSSProperties
   aria-describedby?: string
   aria-details?: string
   aria-errormessage?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean
   channel?: ColorChannel
   colorSpace?: ColorSpace
   contextualHelp?: ReactNode
   defaultValue?: T
   description?: ReactNode
   errorMessage?: ReactNode | (ValidationResult) => ReactNode
   excludeFromTabOrder?: boolean
   form?: string
   id?: string
   isDisabled?: boolean
   isInvalid?: boolean
   isReadOnly?: boolean
   isRequired?: boolean
   isWheelDisabled?: boolean
   label?: ReactNode
   labelAlign?: Alignment = 'start'
   labelPosition?: LabelPosition = 'top'
   name?: string
   necessityIndicator?: NecessityIndicator = 'icon'
   onBeforeInput?: FormEventHandler<T>
   onBlur?: (FocusEvent<Target>) => void
   onChange?: (Color | null) => void
   onCompositionEnd?: CompositionEventHandler<T>
   onCompositionStart?: CompositionEventHandler<T>
   onCompositionUpdate?: CompositionEventHandler<T>
   onCopy?: ClipboardEventHandler<T>
   onCut?: ClipboardEventHandler<T>
   onFocus?: (FocusEvent<Target>) => void
   onFocusChange?: (boolean) => void
   onInput?: FormEventHandler<T>
   onKeyDown?: (KeyboardEvent) => void
   onKeyUp?: (KeyboardEvent) => void
   onPaste?: ClipboardEventHandler<T>
   onSelect?: ReactEventHandler<T>
   placeholder?: string
+  prefix?: ReactNode
   size?: 'S' | 'M' | 'L' | 'XL' = 'M'
   slot?: string | null
   styles?: StylesProp
   validate?: (Color | null) => ValidationError | boolean | null | undefined
   value?: T
 }

/@react-spectrum/s2:ComboBox

 ComboBox <T extends {}> {
   UNSAFE_className?: UnsafeClassName
   UNSAFE_style?: CSSProperties
   align?: 'start' | 'end' = 'start'
   allowsCustomValue?: boolean
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean
   children: ReactNode | ({}) => ReactNode
   contextualHelp?: ReactNode
   defaultInputValue?: string
   defaultItems?: Iterable<T>
   defaultSelectedKey?: Key | null
   dependencies?: ReadonlyArray<any>
   description?: ReactNode
   direction?: 'bottom' | 'top' = 'bottom'
   disabledKeys?: Iterable<Key>
   errorMessage?: ReactNode | (ValidationResult) => ReactNode
   form?: string
   formValue?: 'text' | 'key' = 'key'
   id?: string
   inputValue?: string
   isDisabled?: boolean
   isInvalid?: boolean
   isReadOnly?: boolean
   isRequired?: boolean
   items?: Iterable<T>
   label?: ReactNode
   labelAlign?: Alignment = 'start'
   labelPosition?: LabelPosition = 'top'
   loadingState?: LoadingState
   menuTrigger?: MenuTriggerAction = 'input'
   menuWidth?: number
   name?: string
   necessityIndicator?: NecessityIndicator = 'icon'
   onBlur?: (FocusEvent<HTMLInputElement>) => void
   onFocus?: (FocusEvent<HTMLInputElement>) => void
   onFocusChange?: (boolean) => void
   onInputChange?: (string) => void
   onKeyDown?: (KeyboardEvent) => void
   onKeyUp?: (KeyboardEvent) => void
   onLoadMore?: () => any
   onOpenChange?: (boolean, MenuTriggerAction) => void
   onSelectionChange?: (Key | null) => void
   placeholder?: string
+  prefix?: ReactNode
   selectedKey?: Key | null
   shouldFlip?: boolean = true
   shouldFocusWrap?: boolean
   size?: 'S' | 'M' | 'L' | 'XL' = 'M'
   styles?: StylesProp
   validate?: (ComboBoxValidationValue<SelectionMode>) => ValidationError | boolean | null | undefined
   validationBehavior?: 'native' | 'aria' = 'native'
 }

/@react-spectrum/s2:NumberField

 NumberField {
   UNSAFE_className?: UnsafeClassName
   UNSAFE_style?: CSSProperties
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean
   commitBehavior?: 'snap' | 'validate' = 'snap'
   contextualHelp?: ReactNode
   decrementAriaLabel?: string
   defaultValue?: number
   description?: ReactNode
   errorMessage?: ReactNode | (ValidationResult) => ReactNode
   form?: string
   formatOptions?: Intl.NumberFormatOptions
   hideStepper?: boolean = false
   id?: string
   incrementAriaLabel?: string
   isDisabled?: boolean
   isInvalid?: boolean
   isReadOnly?: boolean
   isRequired?: boolean
   isWheelDisabled?: boolean
   label?: ReactNode
   labelAlign?: Alignment = 'start'
   labelPosition?: LabelPosition = 'top'
   maxValue?: number
   minValue?: number
   name?: string
   necessityIndicator?: NecessityIndicator = 'icon'
   onBeforeInput?: FormEventHandler<T>
   onBlur?: (FocusEvent<Target>) => void
   onChange?: (T) => void
   onCompositionEnd?: CompositionEventHandler<T>
   onCompositionStart?: CompositionEventHandler<T>
   onCompositionUpdate?: CompositionEventHandler<T>
   onCopy?: ClipboardEventHandler<T>
   onCut?: ClipboardEventHandler<T>
   onFocus?: (FocusEvent<Target>) => void
   onFocusChange?: (boolean) => void
   onInput?: FormEventHandler<T>
   onKeyDown?: (KeyboardEvent) => void
   onKeyUp?: (KeyboardEvent) => void
   onPaste?: ClipboardEventHandler<T>
   onSelect?: ReactEventHandler<T>
   placeholder?: string
+  prefix?: ReactNode
   size?: 'S' | 'M' | 'L' | 'XL' = 'M'
   slot?: string | null
   step?: number
   styles?: StylesProp
   validationBehavior?: 'native' | 'aria' = 'native'
   value?: number
 }

/@react-spectrum/s2:ColorFieldProps

 ColorFieldProps {
   UNSAFE_className?: UnsafeClassName
   UNSAFE_style?: CSSProperties
   aria-describedby?: string
   aria-details?: string
   aria-errormessage?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean
   channel?: ColorChannel
   colorSpace?: ColorSpace
   contextualHelp?: ReactNode
   defaultValue?: T
   description?: ReactNode
   errorMessage?: ReactNode | (ValidationResult) => ReactNode
   excludeFromTabOrder?: boolean
   form?: string
   id?: string
   isDisabled?: boolean
   isInvalid?: boolean
   isReadOnly?: boolean
   isRequired?: boolean
   isWheelDisabled?: boolean
   label?: ReactNode
   labelAlign?: Alignment = 'start'
   labelPosition?: LabelPosition = 'top'
   name?: string
   necessityIndicator?: NecessityIndicator = 'icon'
   onBeforeInput?: FormEventHandler<T>
   onBlur?: (FocusEvent<Target>) => void
   onChange?: (Color | null) => void
   onCompositionEnd?: CompositionEventHandler<T>
   onCompositionStart?: CompositionEventHandler<T>
   onCompositionUpdate?: CompositionEventHandler<T>
   onCopy?: ClipboardEventHandler<T>
   onCut?: ClipboardEventHandler<T>
   onFocus?: (FocusEvent<Target>) => void
   onFocusChange?: (boolean) => void
   onInput?: FormEventHandler<T>
   onKeyDown?: (KeyboardEvent) => void
   onKeyUp?: (KeyboardEvent) => void
   onPaste?: ClipboardEventHandler<T>
   onSelect?: ReactEventHandler<T>
   placeholder?: string
+  prefix?: ReactNode
   size?: 'S' | 'M' | 'L' | 'XL' = 'M'
   slot?: string | null
   styles?: StylesProp
   validate?: (Color | null) => ValidationError | boolean | null | undefined
   value?: T
 }

/@react-spectrum/s2:ComboBoxProps

 ComboBoxProps <T extends {}> {
   UNSAFE_className?: UnsafeClassName
   UNSAFE_style?: CSSProperties
   align?: 'start' | 'end' = 'start'
   allowsCustomValue?: boolean
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean
   children: ReactNode | ({}) => ReactNode
   contextualHelp?: ReactNode
   defaultInputValue?: string
   defaultItems?: Iterable<T>
   defaultSelectedKey?: Key | null
   dependencies?: ReadonlyArray<any>
   description?: ReactNode
   direction?: 'bottom' | 'top' = 'bottom'
   disabledKeys?: Iterable<Key>
   errorMessage?: ReactNode | (ValidationResult) => ReactNode
   form?: string
   formValue?: 'text' | 'key' = 'key'
   id?: string
   inputValue?: string
   isDisabled?: boolean
   isInvalid?: boolean
   isReadOnly?: boolean
   isRequired?: boolean
   items?: Iterable<T>
   label?: ReactNode
   labelAlign?: Alignment = 'start'
   labelPosition?: LabelPosition = 'top'
   loadingState?: LoadingState
   menuTrigger?: MenuTriggerAction = 'input'
   menuWidth?: number
   name?: string
   necessityIndicator?: NecessityIndicator = 'icon'
   onBlur?: (FocusEvent<HTMLInputElement>) => void
   onFocus?: (FocusEvent<HTMLInputElement>) => void
   onFocusChange?: (boolean) => void
   onInputChange?: (string) => void
   onKeyDown?: (KeyboardEvent) => void
   onKeyUp?: (KeyboardEvent) => void
   onLoadMore?: () => any
   onOpenChange?: (boolean, MenuTriggerAction) => void
   onSelectionChange?: (Key | null) => void
   placeholder?: string
+  prefix?: ReactNode
   selectedKey?: Key | null
   shouldFlip?: boolean = true
   shouldFocusWrap?: boolean
   size?: 'S' | 'M' | 'L' | 'XL' = 'M'
   styles?: StylesProp
   validate?: (ComboBoxValidationValue<SelectionMode>) => ValidationError | boolean | null | undefined
   validationBehavior?: 'native' | 'aria' = 'native'
 }

@snowystinger snowystinger mentioned this pull request May 5, 2026
5 tasks
Comment on lines +105 to +106
<Provider values={[[IconContext, {styles: style({size: fontRelative(20), '--iconPrimary': {type: 'fill', value: 'currentColor'}})}]]}>
<CenterBaseline id={prefixId} styles={style({minWidth: 20, color: 'gray-600', flexShrink: 0, marginEnd: 'text-to-visual'})}>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should the size scale with the tshirt size? Or is that completely up to the user to handle?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're asking about this from the description?

It center baselines everything automatically for alignment. Should it also set size on avatar and swatch? that's more things that each field has to pull in.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right, sorry I should've been more specific. Avatar seems more troublesome since it doesn't map directly to the tshirt sizing but maybe we can make that mapping for the user. ColorSwatch does have tshirt sizing so it feels like we could automatically provide that via context. Alternatively, we could have prefix accept a render function or something so we could pass the size down via that, might be more flexible depending on just what exactly we wanna allow a user to put in the prefix slot (maybe overkill?)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was unsure how far to carry it. It would mean that each of these components would need to import more code, so that seemed less ideal. Icons are pretty standard already.

Copy link
Copy Markdown
Member

@LFDanLu LFDanLu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm ok with the current API since the user can still scale up the swatch/avatar/arbitrary element if they want to. We can add auto scaling for those later if need be

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants