diff --git a/packages/@react-spectrum/s2/src/CenterBaseline.tsx b/packages/@react-spectrum/s2/src/CenterBaseline.tsx index 58e5fecc998..6112410fed8 100644 --- a/packages/@react-spectrum/s2/src/CenterBaseline.tsx +++ b/packages/@react-spectrum/s2/src/CenterBaseline.tsx @@ -12,11 +12,13 @@ import {css} from '../style/style-macro' with {type: 'macro'}; import {CSSProperties, ReactNode} from 'react'; +import {DOMAttributes} from '@react-types/shared'; +import {filterDOMProps} from 'react-aria/filterDOMProps'; import {mergeStyles} from '../style/runtime'; import {style} from '../style' with {type: 'macro'}; import {StyleString} from '../style/types'; -interface CenterBaselineProps { +interface CenterBaselineProps extends DOMAttributes { style?: CSSProperties, styles?: StyleString, children: ReactNode, @@ -29,8 +31,10 @@ const styles = style({ }); export function CenterBaseline(props: CenterBaselineProps): ReactNode { + let domProps = filterDOMProps(props); return (
diff --git a/packages/@react-spectrum/s2/src/ColorField.tsx b/packages/@react-spectrum/s2/src/ColorField.tsx index b087b60d4de..431a60684ba 100644 --- a/packages/@react-spectrum/s2/src/ColorField.tsx +++ b/packages/@react-spectrum/s2/src/ColorField.tsx @@ -12,16 +12,20 @@ import {ColorField as AriaColorField, ColorFieldProps as AriaColorFieldProps} from 'react-aria-components/ColorField'; -import {ContextValue} from 'react-aria-components/slots'; -import {createContext, forwardRef, Ref, useContext, useImperativeHandle, useRef} from 'react'; +import {CenterBaseline} from './CenterBaseline'; +import {ContextValue, Provider} from 'react-aria-components/slots'; +import {createContext, forwardRef, ReactNode, Ref, useContext, useImperativeHandle, useRef} from 'react'; import {createFocusableRef} from './useDOMRef'; import {field, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; import {FieldErrorIcon, FieldGroup, FieldLabel, HelpText, Input} from './Field'; +import {fontRelative, style} from '../style' with {type: 'macro'}; import {FormContext, useFormProps} from './Form'; import {GlobalDOMAttributes, HelpTextProps, SpectrumLabelableProps} from '@react-types/shared'; -import {InputProps} from 'react-aria-components/Input'; -import {style} from '../style' with {type: 'macro'}; +import {IconContext} from './Icon'; +import {InputContext, InputProps} from 'react-aria-components/Input'; +import {mergeRefs} from 'react-aria/mergeRefs'; import {TextFieldRef} from './TextField'; +import {useId} from 'react-aria/useId'; import {useSpectrumContextProps} from './useSpectrumContextProps'; export interface ColorFieldProps extends Omit, StyleProps, SpectrumLabelableProps, HelpTextProps, Pick { @@ -30,7 +34,11 @@ export interface ColorFieldProps extends Omit, TextFieldRef>>(null); @@ -70,6 +78,7 @@ export const ColorField = forwardRef(function ColorField(props: ColorFieldProps, } })); + let prefixId = useId(); return ( - + {props.prefix ? ( + + + {props.prefix} + + + ) : null} + + {ctx => ( + + + + )} + {isInvalid && } extends Omit, 'children' | 'style' | 'className' | 'render' | 'defaultFilter' | 'allowsEmptyCollection' | 'selectionMode' | 'selectedKey' | 'defaultSelectedKey' | 'onSelectionChange' | 'value' | 'defaultValue' | 'onChange' | keyof GlobalDOMAttributes>, @@ -581,6 +586,7 @@ const ComboboxInner = forwardRef(function ComboboxInner(props: ComboBoxProps @@ -607,9 +613,21 @@ const ComboboxInner = forwardRef(function ComboboxInner(props: ComboBoxProps + {props.prefix ? ( + + + {props.prefix} + + + ) : null} {ctx => ( - + )} diff --git a/packages/@react-spectrum/s2/src/NumberField.tsx b/packages/@react-spectrum/s2/src/NumberField.tsx index f2478553fc4..936c4ed258c 100644 --- a/packages/@react-spectrum/s2/src/NumberField.tsx +++ b/packages/@react-spectrum/s2/src/NumberField.tsx @@ -13,8 +13,9 @@ import Add from '../ui-icons/Add'; import {ButtonProps as AriaButtonProps, ButtonContext, ButtonRenderProps} from 'react-aria-components/Button'; import {NumberField as AriaNumberField, NumberFieldProps as AriaNumberFieldProps} from 'react-aria-components/NumberField'; -import {baseColor, space, style} from '../style' with {type: 'macro'}; -import {ContextValue, useContextProps} from 'react-aria-components/slots'; +import {baseColor, fontRelative, space, style} from '../style' with {type: 'macro'}; +import {CenterBaseline} from './CenterBaseline'; +import {ContextValue, Provider, useContextProps} from 'react-aria-components/slots'; import {controlBorderRadius, field, fieldInput, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; import {createContext, CSSProperties, ForwardedRef, forwardRef, ReactNode, Ref, useContext, useImperativeHandle, useMemo, useRef} from 'react'; import {createFocusableRef} from './useDOMRef'; @@ -23,6 +24,7 @@ import {FieldErrorIcon, FieldGroup, FieldLabel, HelpText, Input} from './Field'; import {filterDOMProps} from 'react-aria/filterDOMProps'; import {FormContext, useFormProps} from './Form'; import {GlobalDOMAttributes, HelpTextProps, SpectrumLabelableProps} from '@react-types/shared'; +import {IconContext} from './Icon'; import {InputContext, InputProps} from 'react-aria-components/Input'; import {mergeProps} from 'react-aria/mergeProps'; import {mergeRefs} from 'react-aria/mergeRefs'; @@ -31,6 +33,7 @@ import {TextFieldRef} from './TextField'; import {useButton} from 'react-aria/useButton'; import {useFocusRing} from 'react-aria/useFocusRing'; import {useHover} from 'react-aria/useHover'; +import {useId} from 'react-aria/useId'; import {useSpectrumContextProps} from './useSpectrumContextProps'; @@ -50,7 +53,11 @@ export interface NumberFieldProps extends * * @default 'M' */ - size?: 'S' | 'M' | 'L' | 'XL' + size?: 'S' | 'M' | 'L' | 'XL', + /** + * The prefix to display in the NumberField. A non-interactive element that appears before the input. + */ + prefix?: ReactNode } export const NumberFieldContext = createContext, TextFieldRef>>(null); @@ -173,6 +180,7 @@ export const NumberField = forwardRef(function NumberField(props: NumberFieldPro } })); + let prefixId = useId(); return ( + {props.prefix ? ( + + + {props.prefix} + + + ) : null} {ctx => ( - + )} diff --git a/packages/@react-spectrum/s2/src/TextField.tsx b/packages/@react-spectrum/s2/src/TextField.tsx index 4c6e2d8ff24..817071fbe63 100644 --- a/packages/@react-spectrum/s2/src/TextField.tsx +++ b/packages/@react-spectrum/s2/src/TextField.tsx @@ -11,12 +11,11 @@ */ import {TextArea as AriaTextArea, TextAreaContext as AriaTextAreaContext} from 'react-aria-components/TextArea'; -import {TextContext as AriaTextContext} from 'react-aria-components/Text'; import {TextField as AriaTextField, TextFieldProps as AriaTextFieldProps} from 'react-aria-components/TextField'; -import {centerBaseline} from './CenterBaseline'; +import {CenterBaseline} from './CenterBaseline'; import {centerPadding, fontRelative, style} from '../style' with {type: 'macro'}; import {composeRenderProps} from 'react-aria-components/composeRenderProps'; -import {ContextValue, DEFAULT_SLOT, Provider, useSlottedContext} from 'react-aria-components/slots'; +import {ContextValue, Provider, useSlottedContext} from 'react-aria-components/slots'; import {controlSize, field, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; import {createContext, forwardRef, ReactNode, Ref, useContext, useImperativeHandle, useRef} from 'react'; import {createFocusableRef} from './useDOMRef'; @@ -27,7 +26,8 @@ import {IconContext} from './Icon'; import {InputContext, InputProps} from 'react-aria-components/Input'; import {mergeRefs} from 'react-aria/mergeRefs'; import {StyleString} from '../style/types'; -import {Text, TextContext} from './Content'; +import {TextContext} from './Content'; +import {useId} from 'react-aria/useId'; import {useSpectrumContextProps} from './useSpectrumContextProps'; export interface TextFieldRef extends FocusableRefValue { @@ -43,7 +43,8 @@ export interface TextFieldProps extends Omit) { [props, ref] = useSpectrumContextProps(props, ref, TextFieldContext); + let prefixId = useId(); return ( @@ -90,7 +93,7 @@ export const TextArea = forwardRef(function TextArea(props: TextAreaProps, ref: ); }); -export const TextFieldBase = forwardRef(function TextFieldBase(props: TextFieldProps & {children: ReactNode, fieldGroupCss?: StyleString}, ref: Ref>) { +export const TextFieldBase = forwardRef(function TextFieldBase(props: TextFieldProps & {children: ReactNode, fieldGroupCss?: StyleString, prefixId?: string}, ref: Ref>) { let inputRef = useRef(null); let domRef = useRef(null); let formContext = useContext(FormContext); @@ -148,7 +151,6 @@ export const TextFieldBase = forwardRef(function TextFieldBase(props: TextFieldP -
- {typeof props.prefix === 'string' ? {props.prefix} : props.prefix} -
+ + {props.prefix} +
) : null } {ctx => ( - + {children} )} diff --git a/packages/@react-spectrum/s2/stories/ColorField.stories.tsx b/packages/@react-spectrum/s2/stories/ColorField.stories.tsx index b72ed550603..6a6a56e2208 100644 --- a/packages/@react-spectrum/s2/stories/ColorField.stories.tsx +++ b/packages/@react-spectrum/s2/stories/ColorField.stories.tsx @@ -10,12 +10,15 @@ * governing permissions and limitations under the License. */ -import {ColorField} from '../src/ColorField'; +import {Color} from 'react-aria-components/ColorField'; +import {ColorField, ColorFieldProps} from '../src/ColorField'; +import {ColorSwatch} from '../src/ColorSwatch'; import {Content, Footer, Heading, Text} from '../src/Content'; import {ContextualHelp} from '../src/ContextualHelp'; import {Link} from '../src/Link'; import type {Meta, StoryObj} from '@storybook/react'; +import {useState} from 'react'; const meta: Meta = { component: ColorField, @@ -72,3 +75,15 @@ export const ContextualHelpExample: Story = { label: 'Color' } }; + +function ColorSwatchExample(props: ColorFieldProps) { + let [color, setColor] = useState(null); + return } />; +} + +export const WithPrefix: Story = { + render: (args) => , + args: { + label: 'Color' + } +}; diff --git a/packages/@react-spectrum/s2/stories/ComboBox.stories.tsx b/packages/@react-spectrum/s2/stories/ComboBox.stories.tsx index 4498c2ddcc6..13ae3206cba 100644 --- a/packages/@react-spectrum/s2/stories/ComboBox.stories.tsx +++ b/packages/@react-spectrum/s2/stories/ComboBox.stories.tsx @@ -393,3 +393,20 @@ export const ComboboxInsideDialog: Story = { ), args: Example.args }; + + +export const WithPrefix: Story = { + render: (args: ComboBoxProps) => ( + + Chocolate + Mint + Strawberry + Vanilla + Chocolate Chip Cookie Dough + + ), + args: { + prefix: , + label: 'User ice cream flavor' + } +}; diff --git a/packages/@react-spectrum/s2/stories/NumberField.stories.tsx b/packages/@react-spectrum/s2/stories/NumberField.stories.tsx index ea2d2d56c37..4c6e54296a2 100644 --- a/packages/@react-spectrum/s2/stories/NumberField.stories.tsx +++ b/packages/@react-spectrum/s2/stories/NumberField.stories.tsx @@ -102,3 +102,14 @@ export const ContextualHelpExample: Story = { label: 'Quantity' } }; + +export const WithPrefix: Story = { + render: (args) => ( + + ), + args: { + label: 'Value', + placeholder: '0.00', + prefix: 'USD' + } +}; diff --git a/packages/@react-spectrum/s2/stories/TextField.stories.tsx b/packages/@react-spectrum/s2/stories/TextField.stories.tsx index f23e5a55382..80222b0de01 100644 --- a/packages/@react-spectrum/s2/stories/TextField.stories.tsx +++ b/packages/@react-spectrum/s2/stories/TextField.stories.tsx @@ -10,7 +10,9 @@ * governing permissions and limitations under the License. */ +import {Avatar} from '../src/Avatar'; import {Button} from '../src/Button'; +import {ColorSwatch} from '../src/ColorSwatch'; import {Content, Footer, Heading, Text} from '../src/Content'; import {ContextualHelp} from '../src/ContextualHelp'; import {Form} from '../src/Form'; @@ -181,6 +183,8 @@ export const TextFieldWithAddons: StoryTextField = { } placeholder="username" /> + } placeholder="username" /> + } placeholder="#FF00FF" /> ), diff --git a/packages/@react-spectrum/s2/test/ColorField.test.tsx b/packages/@react-spectrum/s2/test/ColorField.test.tsx new file mode 100644 index 00000000000..d1e72b7f8e2 --- /dev/null +++ b/packages/@react-spectrum/s2/test/ColorField.test.tsx @@ -0,0 +1,26 @@ +/* + * Copyright 2025 Adobe. All rights reserved. + * This file is licensed to you 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 REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {ColorField} from '../src/ColorField'; +import {render} from '@react-spectrum/test-utils-internal'; + +describe('ColorField', () => { + it('should label the input with the prefix', () => { + let {getByRole} = render( + + ); + + let input = getByRole('textbox'); + let labels = input.getAttribute('aria-labelledby')?.split(' '); + expect(document.getElementById(labels![1])).toHaveTextContent('Prefix'); + }); +}); diff --git a/packages/@react-spectrum/s2/test/Combobox.test.tsx b/packages/@react-spectrum/s2/test/Combobox.test.tsx index f925a5a137d..a4777e09c73 100644 --- a/packages/@react-spectrum/s2/test/Combobox.test.tsx +++ b/packages/@react-spectrum/s2/test/Combobox.test.tsx @@ -259,4 +259,18 @@ describe('Combobox', () => { await user.click(backdrop!); expect(dialogTester.dialog).toBeNull(); }); + + it('should label the input with the prefix', () => { + let {getByRole} = render( + + Item 1 + Item 2 + Item 3 + + ); + + let input = getByRole('combobox'); + let labels = input.getAttribute('aria-labelledby')?.split(' '); + expect(document.getElementById(labels![1])).toHaveTextContent('Prefix'); + }); }); diff --git a/packages/@react-spectrum/s2/test/NumberField.test.tsx b/packages/@react-spectrum/s2/test/NumberField.test.tsx new file mode 100644 index 00000000000..cb536c9be47 --- /dev/null +++ b/packages/@react-spectrum/s2/test/NumberField.test.tsx @@ -0,0 +1,26 @@ +/* + * Copyright 2025 Adobe. All rights reserved. + * This file is licensed to you 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 REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {NumberField} from '../src/NumberField'; +import {render} from '@react-spectrum/test-utils-internal'; + +describe('NumberField', () => { + it('should label the input with the prefix', () => { + let {getByRole} = render( + + ); + + let input = getByRole('textbox'); + let labels = input.getAttribute('aria-labelledby')?.split(' '); + expect(document.getElementById(labels![1])).toHaveTextContent('Prefix'); + }); +}); diff --git a/packages/@react-spectrum/s2/test/TextField.test.tsx b/packages/@react-spectrum/s2/test/TextField.test.tsx index 10e27add725..aa7552590f8 100644 --- a/packages/@react-spectrum/s2/test/TextField.test.tsx +++ b/packages/@react-spectrum/s2/test/TextField.test.tsx @@ -11,7 +11,7 @@ */ import {fireEvent, render} from '@react-spectrum/test-utils-internal'; -import {TextArea} from '../src/TextField'; +import {TextArea, TextField} from '../src/TextField'; describe('TextField', () => { it('should focus textarea when tapping invalid icon', async () => { @@ -29,4 +29,14 @@ describe('TextField', () => { expect(document.activeElement).toBe(textarea); }); + + it('should label the input with the prefix', () => { + let {getByRole} = render( + + ); + + let input = getByRole('textbox'); + let labels = input.getAttribute('aria-labelledby')?.split(' '); + expect(document.getElementById(labels![1])).toHaveTextContent('Prefix'); + }); });