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');
+ });
});