diff --git a/packages/@react-spectrum/s2/chromatic/ColorField.stories.tsx b/packages/@react-spectrum/s2/chromatic/ColorField.stories.tsx index 2e1ca4d9a09..2cae147dade 100644 --- a/packages/@react-spectrum/s2/chromatic/ColorField.stories.tsx +++ b/packages/@react-spectrum/s2/chromatic/ColorField.stories.tsx @@ -11,6 +11,7 @@ */ import {ColorField, ColorFieldProps} from '../src/ColorField'; +import {ColorSwatch} from '../exports'; import {generateComboChunks, shortName} from './utils'; import type {Meta, StoryObj} from '@storybook/react'; import {ReactElement} from 'react'; @@ -144,3 +145,11 @@ export const LabelPositionTopPt5: StoryObj = { }; // Skipped the contextual help stories from here on out since its all shared + +export const ExampleWithPrefix: StoryObj = { + render: args => , + args: { + label: 'Color', + prefix: + } +}; diff --git a/packages/@react-spectrum/s2/chromatic/Combobox.stories.tsx b/packages/@react-spectrum/s2/chromatic/Combobox.stories.tsx index 19ce8eb4ad9..50da3be60b4 100644 --- a/packages/@react-spectrum/s2/chromatic/Combobox.stories.tsx +++ b/packages/@react-spectrum/s2/chromatic/Combobox.stories.tsx @@ -22,6 +22,7 @@ import { WithAvatars, WithIcons } from '../stories/ComboBox.stories'; +import {Avatar} from '../exports'; import {ComboBox} from '../src/ComboBox'; import {expect} from '@storybook/jest'; import type {Meta, StoryObj} from '@storybook/react'; @@ -192,3 +193,10 @@ export const Filtering: StoryObj = { ); } }; + +export const WithPrefix: Story = { + ...Example, + args: { + prefix: + } +}; diff --git a/packages/@react-spectrum/s2/chromatic/NumberField.stories.tsx b/packages/@react-spectrum/s2/chromatic/NumberField.stories.tsx index c109055df57..61378db7142 100644 --- a/packages/@react-spectrum/s2/chromatic/NumberField.stories.tsx +++ b/packages/@react-spectrum/s2/chromatic/NumberField.stories.tsx @@ -111,3 +111,13 @@ export const ContextualHelpExample: Story = { ) } }; + +export const ExampleWithPrefix: Story = { + render: args => , + args: { + label: 'Price', + prefix: 'USD', + formatOptions: {style: 'currency', currency: 'USD', currencyDisplay: 'narrowSymbol'}, + placeholder: '0.00' + } +}; diff --git a/packages/@react-spectrum/s2/src/CenterBaseline.tsx b/packages/@react-spectrum/s2/src/CenterBaseline.tsx index 1b0bb224b02..84d941ef610 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 e4b6d0548a8..e819c353c40 100644 --- a/packages/@react-spectrum/s2/src/ColorField.tsx +++ b/packages/@react-spectrum/s2/src/ColorField.tsx @@ -14,15 +14,23 @@ 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 { + 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 {FormContext, useFormProps} from './Form'; import {GlobalDOMAttributes, HelpTextProps, SpectrumLabelableProps} from '@react-types/shared'; -import {InputProps} from 'react-aria-components/Input'; +import {InputContext, InputProps} from 'react-aria-components/Input'; +import {mergeRefs} from 'react-aria/mergeRefs'; import {style} from '../style' with {type: 'macro'}; import {TextFieldRef} from './TextField'; import {useSpectrumContextProps} from './useSpectrumContextProps'; @@ -43,6 +51,11 @@ export interface ColorFieldProps * @default 'M' */ size?: 'S' | 'M' | 'L' | 'XL'; + /** + * The prefix to display in the ColorField. A non-interactive element that appears before the + * input. + */ + prefix?: ReactNode; } export const ColorFieldContext = @@ -114,8 +127,18 @@ export const ColorField = forwardRef(function ColorField( contextualHelp={props.contextualHelp}> {label} - - + + + {ctx => ( + + + + )} + {isInvalid && } extends @@ -702,6 +706,7 @@ const ComboboxInner = forwardRef(function ComboboxInner( {label} {ctx => ( - + )} diff --git a/packages/@react-spectrum/s2/src/Field.tsx b/packages/@react-spectrum/s2/src/Field.tsx index 128d121cc2d..992810ecdc8 100644 --- a/packages/@react-spectrum/s2/src/Field.tsx +++ b/packages/@react-spectrum/s2/src/Field.tsx @@ -26,15 +26,19 @@ import { UnsafeStyles } from './style-utils' with {type: 'macro'}; import {FieldError, FieldErrorProps} from 'react-aria-components/FieldError'; -import {ForwardedRef, forwardRef, ReactNode} from 'react'; +import {ForwardedRef, forwardRef, ReactNode, useContext} from 'react'; import {getEventTarget} from 'react-aria/private/utils/shadowdom/DOMFunctions'; import {Group, GroupProps} from 'react-aria-components/Group'; import {IconContext} from './Icon'; +import { + InputContext, + Input as RACInput, + InputProps as RACInputProps +} from 'react-aria-components/Input'; import intlMessages from '../intl/*.json'; import {Label, LabelProps} from 'react-aria-components/Label'; import {mergeStyles} from '../style/runtime'; import {Provider} from 'react-aria-components/slots'; -import {Input as RACInput, InputProps as RACInputProps} from 'react-aria-components/Input'; // @ts-ignore import {StyleString} from '../style/types'; import {Text} from 'react-aria-components/Text'; @@ -187,11 +191,12 @@ export const FieldLabel = forwardRef(function FieldLabel( }); interface FieldGroupProps - extends Omit, UnsafeStyles { + extends Omit, UnsafeStyles { size?: 'S' | 'M' | 'L' | 'XL'; children: ReactNode; styles?: StyleString; shouldTurnOffFocusRing?: boolean; + prefix?: ReactNode; } const fieldGroupStyles = style({ @@ -240,7 +245,13 @@ export const FieldGroup = forwardRef(function FieldGroup( props: FieldGroupProps, ref: ForwardedRef ) { - let {shouldTurnOffFocusRing, ...otherProps} = props; + let {children, prefix, shouldTurnOffFocusRing, ...otherProps} = props; + let ctx = useContext(InputContext); + let prefixId = useId(); + let newAriaLabelledby = ctx?.['aria-labelledby']; + if (prefix) { + newAriaLabelledby = newAriaLabelledby ? `${newAriaLabelledby} ${prefixId}` : prefixId; + } return ( + }> + {props.prefix ? ( + + + {props.prefix} + + + ) : null} + + {ctx => ( + + {children} + + )} + + ); }); diff --git a/packages/@react-spectrum/s2/src/NumberField.tsx b/packages/@react-spectrum/s2/src/NumberField.tsx index eef3c41bee2..212f9611206 100644 --- a/packages/@react-spectrum/s2/src/NumberField.tsx +++ b/packages/@react-spectrum/s2/src/NumberField.tsx @@ -79,6 +79,11 @@ export interface NumberFieldProps * @default 'M' */ 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 = @@ -239,6 +244,7 @@ export const NumberField = forwardRef(function NumberField( {label} {ctx => ( + value={{ + ...ctx, + ref: mergeRefs((ctx as any)?.ref, inputRef) + }}> )} diff --git a/packages/@react-spectrum/s2/src/TextField.tsx b/packages/@react-spectrum/s2/src/TextField.tsx index 98850c5b473..8e5a578e9cb 100644 --- a/packages/@react-spectrum/s2/src/TextField.tsx +++ b/packages/@react-spectrum/s2/src/TextField.tsx @@ -14,15 +14,13 @@ 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 {centerPadding, fontRelative, style} from '../style' with {type: 'macro'}; +import {centerPadding, 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, @@ -48,11 +46,10 @@ import { SpectrumLabelableProps } from '@react-types/shared'; import {FormContext, useFormProps} from './Form'; -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 {useSpectrumContextProps} from './useSpectrumContextProps'; export interface TextFieldRef< @@ -79,7 +76,8 @@ export interface TextFieldProps */ size?: 'S' | 'M' | 'L' | 'XL'; /** - * The prefix to display in the text field. Either a string or workflow icon. + * The prefix to display in the text field. + * A non-interactive element that appears before the input. */ prefix?: ReactNode; } @@ -194,55 +192,15 @@ export const TextFieldBase = forwardRef(function TextFieldBase( contextualHelp={props.contextualHelp}> {label} - + - {props.prefix ? ( - -
- {typeof props.prefix === 'string' ? {props.prefix} : props.prefix} -
-
- ) : null} {ctx => ( + value={{ + ...ctx, + ref: mergeRefs((ctx as any)?.ref, inputRef) + }}> {children} )} diff --git a/packages/@react-spectrum/s2/stories/ColorField.stories.tsx b/packages/@react-spectrum/s2/stories/ColorField.stories.tsx index 150fd589265..458110a0f55 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, @@ -70,3 +73,22 @@ 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 97819bb3cf4..a2f4250587d 100644 --- a/packages/@react-spectrum/s2/stories/ComboBox.stories.tsx +++ b/packages/@react-spectrum/s2/stories/ComboBox.stories.tsx @@ -411,3 +411,19 @@ 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 d2d8bd25051..2bce18560d4 100644 --- a/packages/@react-spectrum/s2/stories/NumberField.stories.tsx +++ b/packages/@react-spectrum/s2/stories/NumberField.stories.tsx @@ -99,3 +99,17 @@ 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 d78223c5851..7627b43425e 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'; @@ -190,6 +192,18 @@ 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..81b3e2b0f4a --- /dev/null +++ b/packages/@react-spectrum/s2/test/ColorField.test.tsx @@ -0,0 +1,24 @@ +/* + * 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 76c335241a0..c3244e2f749 100644 --- a/packages/@react-spectrum/s2/test/Combobox.test.tsx +++ b/packages/@react-spectrum/s2/test/Combobox.test.tsx @@ -292,4 +292,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..d05fc2f797c --- /dev/null +++ b/packages/@react-spectrum/s2/test/NumberField.test.tsx @@ -0,0 +1,24 @@ +/* + * 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 5fd453535f7..13bea50124e 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 () => { @@ -27,4 +27,12 @@ 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'); + }); }); diff --git a/packages/dev/s2-docs/pages/s2/ColorField.mdx b/packages/dev/s2-docs/pages/s2/ColorField.mdx index 30ded9256b5..531fd2537f5 100644 --- a/packages/dev/s2-docs/pages/s2/ColorField.mdx +++ b/packages/dev/s2-docs/pages/s2/ColorField.mdx @@ -27,22 +27,20 @@ Use the `value` or `defaultValue` prop to set the color value, and `onChange` to ```tsx render "use client"; import {ColorField, type Color} from '@react-spectrum/s2/ColorField'; +import {ColorSwatch} from '@react-spectrum/s2/ColorSwatch'; import {useState} from 'react'; import {parseColor} from '@react-stately/color'; -import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; function Example() { let [value, setValue] = useState(parseColor('#e73623')); return ( -
- -
Current value: {value?.toString('hex')}
-
+ } /> ); } ``` diff --git a/packages/dev/s2-docs/pages/s2/ComboBox.mdx b/packages/dev/s2-docs/pages/s2/ComboBox.mdx index 3cbae5954a0..727c23cdb2f 100644 --- a/packages/dev/s2-docs/pages/s2/ComboBox.mdx +++ b/packages/dev/s2-docs/pages/s2/ComboBox.mdx @@ -34,6 +34,7 @@ import {ComboBox, ComboBoxItem} from '@react-spectrum/s2/ComboBox'; ```tsx render "use client"; import {ComboBox, ComboBoxItem} from '@react-spectrum/s2/ComboBox'; +import Binoculars from '@react-spectrum/s2/icons/Binoculars'; function Example() { let options = [ @@ -50,7 +51,7 @@ function Example() { return ( /*- begin highlight -*/ - + }> {(item) => {item.name}} /*- end highlight -*/ diff --git a/packages/dev/s2-docs/pages/s2/NumberField.mdx b/packages/dev/s2-docs/pages/s2/NumberField.mdx index 748f53d7aa2..ecd008d527e 100644 --- a/packages/dev/s2-docs/pages/s2/NumberField.mdx +++ b/packages/dev/s2-docs/pages/s2/NumberField.mdx @@ -110,6 +110,27 @@ import {NumberField} from '@react-spectrum/s2/NumberField';
``` +## Prefix + +Use the `prefix` prop to display an additional element before the input in the number field. + +```tsx render +"use client"; +import {NumberField} from '@react-spectrum/s2/NumberField'; + +function Example() { + return ( + + ); +} +``` + ## Forms Use the `name` prop to submit the raw number value (not a formatted string) to the server. Set the `isRequired` prop to validate that the user enters a value, or implement custom client or server-side validation. See the [Forms](forms) guide to learn more. diff --git a/packages/dev/s2-docs/pages/s2/TextArea.mdx b/packages/dev/s2-docs/pages/s2/TextArea.mdx index cd514c816b0..63447f33033 100644 --- a/packages/dev/s2-docs/pages/s2/TextArea.mdx +++ b/packages/dev/s2-docs/pages/s2/TextArea.mdx @@ -52,7 +52,7 @@ function Example() { ## Prefix -Use the `prefix` prop to display a prefix in the text area. Either a string or workflow icon. +Use the `prefix` prop to display an additional element before the input in the text area. ```tsx render "use client"; diff --git a/packages/dev/s2-docs/pages/s2/TextField.mdx b/packages/dev/s2-docs/pages/s2/TextField.mdx index c7adbdd5c6f..bd8da618e7b 100644 --- a/packages/dev/s2-docs/pages/s2/TextField.mdx +++ b/packages/dev/s2-docs/pages/s2/TextField.mdx @@ -52,13 +52,14 @@ function Example() { ## Prefix -Use the `prefix` prop to display a prefix in the text field. Either a string or workflow icon. +Use the `prefix` prop to display an additional element before the input in the text field. ```tsx render "use client"; import {TextField} from '@react-spectrum/s2/TextField'; import MentionIcon from '@react-spectrum/s2/icons/Mention'; import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; +import {Avatar} from '@react-spectrum/s2/Avatar'; function Example() { return ( @@ -66,10 +67,12 @@ function Example() { } placeholder="username" /> + } placeholder="contact@example.com" />
); } ``` + ## Forms Use the `name` prop to submit the text value to the server. Set the `isRequired`, `minLength`, `maxLength`, `pattern`, or `type` props to validate the value, or implement custom client or server-side validation. See the [Forms](forms) guide to learn more.